mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Fix for library frames not collapsing (#26827)
* [APM] fixes #26525 - simplified the stackframe grouping algorithm - add support for `stackframe.exclude_from_grouping` - made the rendering more tolerant of edge cases * Made improvements to code readability and added more meaningful test cases
This commit is contained in:
parent
d01aaef850
commit
f1520f855d
6 changed files with 1110 additions and 128 deletions
|
@ -20,6 +20,26 @@ const LibraryFrameToggle = styled.div`
|
|||
user-select: none;
|
||||
`;
|
||||
|
||||
interface LibraryStackFrameProps {
|
||||
codeLanguage?: string;
|
||||
stackframe: Stackframe;
|
||||
}
|
||||
|
||||
const LibraryStackFrame: React.SFC<LibraryStackFrameProps> = ({
|
||||
codeLanguage,
|
||||
stackframe
|
||||
}) => {
|
||||
return hasSourceLines(stackframe) ? (
|
||||
<CodePreview
|
||||
stackframe={stackframe}
|
||||
isLibraryFrame
|
||||
codeLanguage={codeLanguage}
|
||||
/>
|
||||
) : (
|
||||
<FrameHeading stackframe={stackframe} isLibraryFrame />
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
stackframes: Stackframe[];
|
||||
|
@ -27,12 +47,25 @@ interface Props {
|
|||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const LibraryFrames: React.SFC<Props> = ({
|
||||
export const LibraryStackFrames: React.SFC<Props> = ({
|
||||
visible,
|
||||
stackframes,
|
||||
codeLanguage,
|
||||
onClick
|
||||
}) => {
|
||||
if (stackframes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stackframes.length === 1) {
|
||||
return (
|
||||
<LibraryStackFrame
|
||||
codeLanguage={codeLanguage}
|
||||
stackframe={stackframes[0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LibraryFrameToggle>
|
||||
|
@ -44,18 +77,13 @@ export const LibraryFrames: React.SFC<Props> = ({
|
|||
|
||||
<div>
|
||||
{visible &&
|
||||
stackframes.map((stackframe, i) =>
|
||||
hasSourceLines(stackframe) ? (
|
||||
<CodePreview
|
||||
key={i}
|
||||
stackframe={stackframe}
|
||||
isLibraryFrame
|
||||
codeLanguage={codeLanguage}
|
||||
/>
|
||||
) : (
|
||||
<FrameHeading key={i} stackframe={stackframe} isLibraryFrame />
|
||||
)
|
||||
)}
|
||||
stackframes.map((stackframe, i) => (
|
||||
<LibraryStackFrame
|
||||
key={i}
|
||||
codeLanguage={codeLanguage}
|
||||
stackframe={stackframe}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,460 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`stactraceUtils getGroupedStackframes should collapse the library frames into a set of grouped stackframes 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"isLibraryFrame": false,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/server/routes.js",
|
||||
"context": Object {
|
||||
"post": Array [
|
||||
" req.body.lines.forEach(function (line) {",
|
||||
" client.query('SELECT id FROM products WHERE id=$1', [line.id], next())",
|
||||
],
|
||||
"pre": Array [
|
||||
" })",
|
||||
"",
|
||||
],
|
||||
},
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server/routes.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": false,
|
||||
"line": Object {
|
||||
"context": " client.query('SELECT id FROM customers WHERE id=$1', [req.body.customer_id], next())",
|
||||
"number": 307,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": true,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"function": "BoundPool.<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 137,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/generic-pool/lib/generic-pool.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/generic-pool/lib/generic-pool.js",
|
||||
"function": "dispense",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 310,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/generic-pool/lib/generic-pool.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/generic-pool/lib/generic-pool.js",
|
||||
"function": "acquire",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 391,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"function": "BoundPool.<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 111,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"function": "Pool._promiseNoCallback",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 75,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"function": "Pool.connect",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 109,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": false,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/server/db.js",
|
||||
"context": Object {
|
||||
"post": Array [
|
||||
"}",
|
||||
"",
|
||||
],
|
||||
"pre": Array [
|
||||
"",
|
||||
"exports.client = function (cb) {",
|
||||
],
|
||||
},
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server/db.js",
|
||||
"function": "exports.client",
|
||||
"library_frame": false,
|
||||
"line": Object {
|
||||
"context": " pool.connect(cb)",
|
||||
"number": 11,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/server/routes.js",
|
||||
"context": Object {
|
||||
"post": Array [
|
||||
" if (err) return error(err, res)",
|
||||
"",
|
||||
],
|
||||
"pre": Array [
|
||||
" }",
|
||||
"",
|
||||
],
|
||||
},
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server/routes.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": false,
|
||||
"line": Object {
|
||||
"context": " db.client(function (err, client, done) {",
|
||||
"number": 248,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": true,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 95,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/route.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/route.js",
|
||||
"function": "next",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 137,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/route.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/route.js",
|
||||
"function": "dispatch",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 112,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 95,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 281,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "process_params",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 335,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "next",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 275,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 174,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "router",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 47,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 95,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "trim_prefix",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 317,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 284,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "process_params",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 335,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "next",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 275,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": false,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/server.js",
|
||||
"context": Object {
|
||||
"post": Array [
|
||||
" }",
|
||||
"",
|
||||
],
|
||||
"pre": Array [
|
||||
"app.use('/api', function (req, res, next) {",
|
||||
" if (Math.random() > opbeansRedirectProbability) {",
|
||||
],
|
||||
},
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": false,
|
||||
"line": Object {
|
||||
"context": " return next()",
|
||||
"number": 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": true,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 95,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "trim_prefix",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 317,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 284,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "process_params",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 335,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": true,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": true,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "next",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 275,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"isLibraryFrame": true,
|
||||
"stackframes": Array [
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 635,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "next",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 260,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 174,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "router",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 47,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"function": "handle",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 95,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "trim_prefix",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 317,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "<anonymous>",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 284,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"function": "process_params",
|
||||
"library_frame": true,
|
||||
"line": Object {
|
||||
"number": 335,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -5,21 +5,166 @@
|
|||
*/
|
||||
|
||||
import { Stackframe } from '../../../../../typings/APMDoc';
|
||||
import { getCollapsedLibraryFrames, hasSourceLines } from '../stacktraceUtils';
|
||||
import { getGroupedStackframes, hasSourceLines } from '../stacktraceUtils';
|
||||
import stacktracesMock from './stacktraces.json';
|
||||
|
||||
const stackframeMockWithSource = stacktracesMock[0];
|
||||
const stackframeMockWithoutSource = stacktracesMock[1];
|
||||
|
||||
describe('stactraceUtils', () => {
|
||||
describe('getCollapsedLibraryFrames', () => {
|
||||
it('should collapse the library frames into a set of grouped, nested stackframes', () => {
|
||||
const result = getCollapsedLibraryFrames(stacktracesMock as Stackframe[]);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[0].libraryFrame).toBe(false);
|
||||
expect(result[1].libraryFrame).toBe(true);
|
||||
expect(result[1].stackframes).toHaveLength(2); // two nested stackframes
|
||||
expect(result[2].libraryFrame).toBe(false);
|
||||
describe('getGroupedStackframes', () => {
|
||||
it('should collapse the library frames into a set of grouped stackframes', () => {
|
||||
const result = getGroupedStackframes(stacktracesMock as Stackframe[]);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should group stackframes when `library_frame` is identical and `exclude_from_grouping` is false', () => {
|
||||
const stackframes = [
|
||||
{
|
||||
library_frame: false,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-a.txt'
|
||||
},
|
||||
{
|
||||
library_frame: false,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-b.txt'
|
||||
},
|
||||
{
|
||||
library_frame: true,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-c.txt'
|
||||
},
|
||||
{
|
||||
library_frame: true,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-d.txt'
|
||||
}
|
||||
] as Stackframe[];
|
||||
|
||||
const result = getGroupedStackframes(stackframes);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
isLibraryFrame: false,
|
||||
stackframes: [
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-a.txt',
|
||||
library_frame: false
|
||||
},
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-b.txt',
|
||||
library_frame: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
isLibraryFrame: true,
|
||||
stackframes: [
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-c.txt',
|
||||
library_frame: true
|
||||
},
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-d.txt',
|
||||
library_frame: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not group stackframes when `library_frame` is the different', () => {
|
||||
const stackframes = [
|
||||
{
|
||||
library_frame: false,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-a.txt'
|
||||
},
|
||||
{
|
||||
library_frame: true,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-b.txt'
|
||||
}
|
||||
] as Stackframe[];
|
||||
const result = getGroupedStackframes(stackframes);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
isLibraryFrame: false,
|
||||
stackframes: [
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-a.txt',
|
||||
library_frame: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
isLibraryFrame: true,
|
||||
stackframes: [
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-b.txt',
|
||||
library_frame: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not group stackframes when `exclude_from_grouping` is true', () => {
|
||||
const stackframes = [
|
||||
{
|
||||
library_frame: false,
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-a.txt'
|
||||
},
|
||||
{
|
||||
library_frame: false,
|
||||
exclude_from_grouping: true,
|
||||
filename: 'file-b.txt'
|
||||
}
|
||||
] as Stackframe[];
|
||||
const result = getGroupedStackframes(stackframes);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
isLibraryFrame: false,
|
||||
stackframes: [
|
||||
{
|
||||
exclude_from_grouping: false,
|
||||
filename: 'file-a.txt',
|
||||
library_frame: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
isLibraryFrame: false,
|
||||
stackframes: [
|
||||
{
|
||||
exclude_from_grouping: true,
|
||||
filename: 'file-b.txt',
|
||||
library_frame: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty stackframes', () => {
|
||||
const result = getGroupedStackframes([] as Stackframe[]);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle one stackframe', () => {
|
||||
const result = getGroupedStackframes([
|
||||
stacktracesMock[0]
|
||||
] as Stackframe[]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].stackframes).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,67 +1,416 @@
|
|||
[
|
||||
{
|
||||
"library_frame": false,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server/routes.js",
|
||||
"abs_path": "/app/server/routes.js",
|
||||
"line": {
|
||||
"number": 307,
|
||||
"context": " client.query('SELECT id FROM customers WHERE id=$1', [req.body.customer_id], next())"
|
||||
},
|
||||
"function": "<anonymous>",
|
||||
"libraryFrame": false,
|
||||
"excludeFromGrouping": false,
|
||||
"context": {
|
||||
"pre": ["", "app.get('/log-error', function (req, res) {"],
|
||||
"pre": [
|
||||
" })",
|
||||
""
|
||||
],
|
||||
"post": [
|
||||
" if (err) {",
|
||||
" res.status(500).send('could not capture error: ' + err.message)"
|
||||
" req.body.lines.forEach(function (line) {",
|
||||
" client.query('SELECT id FROM products WHERE id=$1', [line.id], next())"
|
||||
]
|
||||
},
|
||||
"line": {
|
||||
"number": 17,
|
||||
"context": " apm.captureError(new Error('foo'), function (err) {"
|
||||
},
|
||||
"filename": "server/coffee.js",
|
||||
"absPath": "/app/server/coffee.js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"function": "get",
|
||||
"libraryFrame": true,
|
||||
"excludeFromGrouping": false,
|
||||
"context": {},
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"line": {
|
||||
"number": 123,
|
||||
"context": ""
|
||||
"number": 137
|
||||
},
|
||||
"filename": "express/get.js",
|
||||
"absPath": "/node_modules/express/get.js"
|
||||
"function": "BoundPool.<anonymous>"
|
||||
},
|
||||
{
|
||||
"function": "use",
|
||||
"libraryFrame": true,
|
||||
"excludeFromGrouping": false,
|
||||
"context": {
|
||||
"pre": ["", "app.use('/log-error', function (req, res) {"],
|
||||
"post": [
|
||||
" if (err) {",
|
||||
" res.status(500).send('could not capture error: ' + err.message)"
|
||||
]
|
||||
},
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/generic-pool/lib/generic-pool.js",
|
||||
"abs_path": "/app/node_modules/generic-pool/lib/generic-pool.js",
|
||||
"line": {
|
||||
"number": 234,
|
||||
"context": " apm.captureError(new Error('foo'), function (err) {"
|
||||
"number": 310
|
||||
},
|
||||
"filename": "express/use.js",
|
||||
"absPath": "/node_modules/express/use.js"
|
||||
"function": "dispense"
|
||||
},
|
||||
{
|
||||
"function": "handleCoffee",
|
||||
"libraryFrame": false,
|
||||
"excludeFromGrouping": false,
|
||||
"context": {
|
||||
"pre": ["", ""],
|
||||
"post": [
|
||||
" reply(getCoffee(req.id));"
|
||||
]
|
||||
},
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/generic-pool/lib/generic-pool.js",
|
||||
"abs_path": "/app/node_modules/generic-pool/lib/generic-pool.js",
|
||||
"line": {
|
||||
"number": 45,
|
||||
"context": " handleCoffee(req => {"
|
||||
"number": 391
|
||||
},
|
||||
"filename": "server/handleCoffee.js",
|
||||
"absPath": "/app/server/handleCoffee.js"
|
||||
"function": "acquire"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"line": {
|
||||
"number": 111
|
||||
},
|
||||
"function": "BoundPool.<anonymous>"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"line": {
|
||||
"number": 75
|
||||
},
|
||||
"function": "Pool._promiseNoCallback"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/pg-pool/index.js",
|
||||
"abs_path": "/app/node_modules/pg-pool/index.js",
|
||||
"line": {
|
||||
"number": 109
|
||||
},
|
||||
"function": "Pool.connect"
|
||||
},
|
||||
{
|
||||
"library_frame": false,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server/db.js",
|
||||
"abs_path": "/app/server/db.js",
|
||||
"line": {
|
||||
"number": 11,
|
||||
"context": " pool.connect(cb)"
|
||||
},
|
||||
"function": "exports.client",
|
||||
"context": {
|
||||
"pre": [
|
||||
"",
|
||||
"exports.client = function (cb) {"
|
||||
],
|
||||
"post": [
|
||||
"}",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"library_frame": false,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "server/routes.js",
|
||||
"abs_path": "/app/server/routes.js",
|
||||
"line": {
|
||||
"number": 248,
|
||||
"context": " db.client(function (err, client, done) {"
|
||||
},
|
||||
"function": "<anonymous>",
|
||||
"context": {
|
||||
"pre": [
|
||||
" }",
|
||||
""
|
||||
],
|
||||
"post": [
|
||||
" if (err) return error(err, res)",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"line": {
|
||||
"number": 95
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/route.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/route.js",
|
||||
"line": {
|
||||
"number": 137
|
||||
},
|
||||
"function": "next"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/route.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/route.js",
|
||||
"line": {
|
||||
"number": 112
|
||||
},
|
||||
"function": "dispatch"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"line": {
|
||||
"number": 95
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 281
|
||||
},
|
||||
"function": "<anonymous>"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 335
|
||||
},
|
||||
"function": "process_params"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 275
|
||||
},
|
||||
"function": "next"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 174
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 47
|
||||
},
|
||||
"function": "router"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"line": {
|
||||
"number": 95
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 317
|
||||
},
|
||||
"function": "trim_prefix"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 284
|
||||
},
|
||||
"function": "<anonymous>"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 335
|
||||
},
|
||||
"function": "process_params"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 275
|
||||
},
|
||||
"function": "next"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": false,
|
||||
"filename": "server.js",
|
||||
"abs_path": "/app/server.js",
|
||||
"line": {
|
||||
"number": 88,
|
||||
"context": " return next()"
|
||||
},
|
||||
"function": "<anonymous>",
|
||||
"context": {
|
||||
"pre": [
|
||||
"app.use('/api', function (req, res, next) {",
|
||||
" if (Math.random() > opbeansRedirectProbability) {"
|
||||
],
|
||||
"post": [
|
||||
" }",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"line": {
|
||||
"number": 95
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 317
|
||||
},
|
||||
"function": "trim_prefix"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 284
|
||||
},
|
||||
"function": "<anonymous>"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 335
|
||||
},
|
||||
"function": "process_params"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": true,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 275
|
||||
},
|
||||
"function": "next"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 635
|
||||
},
|
||||
"function": "<anonymous>"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 260
|
||||
},
|
||||
"function": "next"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 174
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 47
|
||||
},
|
||||
"function": "router"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/layer.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/layer.js",
|
||||
"line": {
|
||||
"number": 95
|
||||
},
|
||||
"function": "handle"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 317
|
||||
},
|
||||
"function": "trim_prefix"
|
||||
},
|
||||
{
|
||||
"exclude_from_grouping": false,
|
||||
"library_frame": true,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 284
|
||||
},
|
||||
"function": "<anonymous>"
|
||||
},
|
||||
{
|
||||
"library_frame": true,
|
||||
"exclude_from_grouping": false,
|
||||
"filename": "node_modules/express/lib/router/index.js",
|
||||
"abs_path": "/app/node_modules/express/lib/router/index.js",
|
||||
"line": {
|
||||
"number": 335
|
||||
},
|
||||
"function": "process_params"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -7,34 +7,29 @@
|
|||
import { EuiTitle } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Stackframe } from '../../../../typings/APMDoc';
|
||||
import { CodePreview } from '../../shared/CodePreview';
|
||||
import { EmptyMessage } from '../../shared/EmptyMessage';
|
||||
// @ts-ignore
|
||||
import { Ellipsis } from '../../shared/Icons';
|
||||
import { FrameHeading } from './FrameHeading';
|
||||
import { LibraryFrames } from './LibraryFrames';
|
||||
import {
|
||||
getCollapsedLibraryFrames,
|
||||
hasSourceLines,
|
||||
StackframeCollapsed
|
||||
} from './stacktraceUtils';
|
||||
import { LibraryStackFrames } from './LibraryStackFrames';
|
||||
import { getGroupedStackframes, hasSourceLines } from './stacktraceUtils';
|
||||
|
||||
interface Props {
|
||||
stackframes?: StackframeCollapsed[];
|
||||
stackframes?: Stackframe[];
|
||||
codeLanguage?: string;
|
||||
}
|
||||
|
||||
interface StateLibraryframes {
|
||||
[i: number]: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
libraryframes: StateLibraryframes;
|
||||
visibilityMap: {
|
||||
[i: number]: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export class Stacktrace extends PureComponent<Props, State> {
|
||||
public state = {
|
||||
libraryframes: {}
|
||||
visibilityMap: {}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -44,23 +39,23 @@ export class Stacktrace extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const hasAnyAppFrames = this.props.stackframes.some(
|
||||
frame => !frame.libraryFrame
|
||||
frame => !frame.library_frame
|
||||
);
|
||||
|
||||
if (!hasAnyAppFrames) {
|
||||
// If there are no app frames available, always show the only existing group
|
||||
this.setState({ libraryframes: { 0: true } });
|
||||
this.setState({ visibilityMap: { 0: true } });
|
||||
}
|
||||
}
|
||||
|
||||
public toggle = (i: number) =>
|
||||
this.setState(({ libraryframes }) => {
|
||||
return { libraryframes: { ...libraryframes, [i]: !libraryframes[i] } };
|
||||
this.setState(({ visibilityMap }) => {
|
||||
return { visibilityMap: { ...visibilityMap, [i]: !visibilityMap[i] } };
|
||||
});
|
||||
|
||||
public render() {
|
||||
const { stackframes = [], codeLanguage } = this.props;
|
||||
const { libraryframes } = this.state as State;
|
||||
const { visibilityMap } = this.state as State;
|
||||
|
||||
if (isEmpty(stackframes)) {
|
||||
return <EmptyMessage heading="No stacktrace available." hideSubheading />;
|
||||
|
@ -71,30 +66,32 @@ export class Stacktrace extends PureComponent<Props, State> {
|
|||
<EuiTitle size="xs">
|
||||
<h3>Stack traces</h3>
|
||||
</EuiTitle>
|
||||
{getCollapsedLibraryFrames(stackframes).map((item, i) => {
|
||||
if (!item.libraryFrame) {
|
||||
if (hasSourceLines(item)) {
|
||||
{getGroupedStackframes(stackframes).map(
|
||||
({ isLibraryFrame, stackframes: groupedStackframes }, i) => {
|
||||
if (isLibraryFrame) {
|
||||
return (
|
||||
<CodePreview
|
||||
<LibraryStackFrames
|
||||
key={i}
|
||||
stackframe={item}
|
||||
visible={visibilityMap[i]}
|
||||
stackframes={groupedStackframes}
|
||||
codeLanguage={codeLanguage}
|
||||
onClick={() => this.toggle(i)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <FrameHeading key={i} stackframe={item} />;
|
||||
return groupedStackframes.map((stackframe, idx) =>
|
||||
hasSourceLines(stackframe) ? (
|
||||
<CodePreview
|
||||
key={idx}
|
||||
stackframe={stackframe}
|
||||
codeLanguage={codeLanguage}
|
||||
/>
|
||||
) : (
|
||||
<FrameHeading key={idx} stackframe={stackframe} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LibraryFrames
|
||||
key={i}
|
||||
visible={libraryframes[i]}
|
||||
stackframes={item.stackframes || []}
|
||||
codeLanguage={codeLanguage}
|
||||
onClick={() => this.toggle(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,35 +7,38 @@
|
|||
import { get, isEmpty } from 'lodash';
|
||||
import { Stackframe } from '../../../../typings/APMDoc';
|
||||
|
||||
export interface StackframeCollapsed extends Stackframe {
|
||||
libraryFrame?: boolean;
|
||||
stackframes?: Stackframe[];
|
||||
interface StackframesGroup {
|
||||
isLibraryFrame: boolean;
|
||||
stackframes: Stackframe[];
|
||||
}
|
||||
|
||||
export function getCollapsedLibraryFrames(
|
||||
function getNextGroupIndex(stackframes: Stackframe[]): number {
|
||||
const isLibraryFrame = Boolean(stackframes[0].library_frame);
|
||||
const groupEndIndex =
|
||||
stackframes.findIndex(
|
||||
stackframe =>
|
||||
isLibraryFrame !== Boolean(stackframe.library_frame) ||
|
||||
Boolean(stackframe.exclude_from_grouping)
|
||||
) || 1;
|
||||
return groupEndIndex === -1 ? stackframes.length : groupEndIndex;
|
||||
}
|
||||
|
||||
export function getGroupedStackframes(
|
||||
stackframes: Stackframe[]
|
||||
): StackframeCollapsed[] {
|
||||
return stackframes.reduce((acc: any, stackframe: StackframeCollapsed) => {
|
||||
if (!stackframe.libraryFrame) {
|
||||
return [...acc, stackframe];
|
||||
}
|
||||
): StackframesGroup[] {
|
||||
if (stackframes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// current stackframe is library frame
|
||||
const prevItem: StackframeCollapsed = acc[acc.length - 1];
|
||||
if (!get(prevItem, 'libraryFrame')) {
|
||||
return [...acc, { libraryFrame: true, stackframes: [stackframe] }];
|
||||
}
|
||||
const nextGroupIndex = getNextGroupIndex(stackframes);
|
||||
|
||||
return [
|
||||
...acc.slice(0, -1),
|
||||
{
|
||||
...prevItem,
|
||||
stackframes: prevItem.stackframes
|
||||
? [...prevItem.stackframes, stackframe]
|
||||
: [stackframe]
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
return [
|
||||
{
|
||||
isLibraryFrame: Boolean(stackframes[0].library_frame),
|
||||
stackframes: stackframes.slice(0, nextGroupIndex)
|
||||
},
|
||||
...getGroupedStackframes(stackframes.slice(nextGroupIndex))
|
||||
];
|
||||
}
|
||||
|
||||
export function hasSourceLines(stackframe: Stackframe) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue