[APM] Link from error to transaction (#27227)

This commit is contained in:
Søren Louv-Jansen 2018-12-20 00:47:13 +01:00 committed by GitHub
parent 7dc673721c
commit 998afde6e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 650 additions and 4248 deletions

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import DetailView from '../index';
import props from './props.json';
import { shallow } from 'enzyme';
import { mockMoment } from '../../../../../utils/testHelpers';
describe('DetailView', () => {
beforeEach(() => {
// Avoid timezone issues
mockMoment();
});
it('should render empty state', () => {
const wrapper = shallow(
<DetailView
errorGroup={[]}
urlParams={props.urlParams}
location={{ state: '' }}
/>
);
expect(wrapper).toMatchSnapshot();
});
it('should render with data', () => {
const wrapper = shallow(
<DetailView
errorGroup={props.errorGroup}
urlParams={props.urlParams}
location={{ state: '' }}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,154 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { RRRRenderResponse } from 'react-redux-request';
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
// @ts-ignore
import { mockMoment } from '../../../../../utils/testHelpers';
import { DetailView } from '../index';
describe('DetailView', () => {
beforeEach(() => {
// Avoid timezone issues
mockMoment();
});
it('should render empty state', () => {
const wrapper = shallow(
<DetailView
errorGroup={{} as any}
urlParams={{}}
location={{ state: '' }}
/>
);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('should render Discover button', () => {
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
args: [],
status: 'SUCCESS',
data: {
occurrencesCount: 10,
error: ({
'@timestamp': 'myTimestamp',
context: {
service: { name: 'myService' },
user: { id: 'myUserId' },
request: { method: 'GET', url: { full: 'myUrl' } }
},
error: { exception: { handled: true } },
transaction: { id: 'myTransactionId', sampled: true }
} as unknown) as APMError
}
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{ state: '' }}
/>
).find('DiscoverErrorButton');
expect(wrapper.exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
it('should render StickyProperties', () => {
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
args: [],
status: 'SUCCESS',
data: {
occurrencesCount: 10,
transaction: ({
trace: { id: 'traceId' },
transaction: {
type: 'myTransactionType',
name: 'myTransactionName',
id: 'myTransactionName'
},
context: {
service: { name: 'myService' },
user: { id: 'myUserId' },
request: { method: 'GET', url: { full: 'myUrl' } }
}
} as unknown) as Transaction,
error: ({
'@timestamp': 'myTimestamp',
context: {
service: { name: 'myService' },
user: { id: 'myUserId' },
request: { method: 'GET', url: { full: 'myUrl' } }
},
error: { exception: { handled: true } },
transaction: { id: 'myTransactionId', sampled: true }
} as unknown) as APMError
}
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{ state: '' }}
/>
).find('StickyProperties');
expect(wrapper.exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
it('should render tabs', () => {
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
args: [],
status: 'SUCCESS',
data: {
occurrencesCount: 10,
error: ({
'@timestamp': 'myTimestamp',
context: { service: {}, user: {}, request: {} }
} as unknown) as APMError
}
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{ state: '' }}
/>
).find('EuiTabs');
expect(wrapper.exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
it('should render TabContent', () => {
const errorGroup: RRRRenderResponse<ErrorGroupAPIResponse> = {
args: [],
status: 'SUCCESS',
data: {
occurrencesCount: 10,
error: ({
'@timestamp': 'myTimestamp',
context: {}
} as unknown) as APMError
}
};
const wrapper = shallow(
<DetailView
errorGroup={errorGroup}
urlParams={{}}
location={{ state: '' }}
/>
).find('TabContent');
expect(wrapper.exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,168 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DetailView should render Discover button 1`] = `
<DiscoverErrorButton
error={
Object {
"@timestamp": "myTimestamp",
"context": Object {
"request": Object {
"method": "GET",
"url": Object {
"full": "myUrl",
},
},
"service": Object {
"name": "myService",
},
"user": Object {
"id": "myUserId",
},
},
"error": Object {
"exception": Object {
"handled": true,
},
},
"transaction": Object {
"id": "myTransactionId",
"sampled": true,
},
}
}
>
<EuiButtonEmpty
color="primary"
iconSide="left"
iconType="discoverApp"
type="button"
>
View 10 occurences in Discover
</EuiButtonEmpty>
</DiscoverErrorButton>
`;
exports[`DetailView should render StickyProperties 1`] = `
<StickyProperties
stickyProperties={
Array [
Object {
"fieldName": "@timestamp",
"label": "Timestamp",
"val": "myTimestamp",
"width": "50%",
},
Object {
"fieldName": "context.request.url.full",
"label": "URL",
"truncated": true,
"val": "myUrl",
"width": "50%",
},
Object {
"fieldName": "context.request.method",
"label": "Request method",
"val": "GET",
"width": "25%",
},
Object {
"fieldName": "error.exception.handled",
"label": "Handled",
"val": true,
"width": "25%",
},
Object {
"fieldName": "transaction.id",
"label": "Transaction sample ID",
"val": <Connect(Component)
hash="/myService/transactions/myTransactionType/myTransactionName"
pathname="/app/apm"
query={
Object {
"traceid": "traceId",
"transactionId": "myTransactionName",
}
}
>
myTransactionName
</Connect(Component)>,
"width": "25%",
},
Object {
"fieldName": "context.user.id",
"label": "User ID",
"val": "myUserId",
"width": "25%",
},
]
}
/>
`;
exports[`DetailView should render TabContent 1`] = `
<TabContent
currentTab="exception_stacktrace"
error={
Object {
"@timestamp": "myTimestamp",
"context": Object {},
}
}
/>
`;
exports[`DetailView should render tabs 1`] = `
<EuiTabs
expand={false}
size="m"
>
<EuiTab
disabled={false}
isSelected={true}
key="exception_stacktrace"
onClick={[Function]}
>
Exception stacktrace
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="request"
onClick={[Function]}
>
Request
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="service"
onClick={[Function]}
>
Service
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="user"
onClick={[Function]}
>
User
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="tags"
onClick={[Function]}
>
Tags
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="custom"
onClick={[Function]}
>
Custom
</EuiTab>
</EuiTabs>
`;

View file

@ -1,934 +0,0 @@
{
"errorGroup": {
"args": {
"serviceName": "opbeans-node",
"errorGroupId": "c00e245c2fbebaf178fc31eeb2bb0250",
"start": "2018-01-09T14:24:42.561Z",
"end": "2018-01-09T14:39:42.561Z"
},
"data": {
"error": {
"@timestamp": "2018-01-09T14:39:00.274Z",
"processor": { "event": "error", "name": "error" },
"error": {
"id": "c5e55dfc-09cc-4e0d-ace3-1ba4233f66eb",
"culprit": "<anonymous> (server/coffee.js)",
"exception": {
"type": "TypeError",
"stacktrace": [
{
"line": {
"number": 9,
"context": " if (req.paarms.level === 11) {"
},
"filename": "server/coffee.js",
"absPath": "/app/server/coffee.js",
"function": "<anonymous>",
"libraryFrame": false,
"context": {
"pre": [
"",
"var express = require('express')",
"var apm = require('elastic-apm-node')",
"",
"var app = module.exports = new express.Router()",
"",
"app.get('/is-it-coffee-time', function (req, res) {"
],
"post": [
" res.send('Of course!')",
" } else {",
" res.send('You can\\'t have any!')",
" }",
"})",
"",
"app.get('/log-error', function (req, res) {"
]
}
},
{
"filename": "node_modules/express/lib/router/layer.js",
"absPath": "/app/node_modules/express/lib/router/layer.js",
"function": "handle",
"libraryFrame": true,
"context": {
"pre": [
"",
" if (fn.length > 3) {",
" // not a standard request handler",
" return next();",
" }",
"",
" try {"
],
"post": [
" } catch (err) {",
" next(err);",
" }",
"};",
"",
"/**",
" * Check if this route matches `path`, if so"
]
},
"line": { "number": 95, "context": " fn(req, res, next);" }
},
{
"function": "next",
"libraryFrame": true,
"context": {
"pre": [
" if (layer.method && layer.method !== method) {",
" return next(err);",
" }",
"",
" if (err) {",
" layer.handle_error(err, req, res, next);",
" } else {"
],
"post": [
" }",
" }",
"};",
"",
"/**",
" * Add a handler for all HTTP verbs to this route.",
" *"
]
},
"line": {
"number": 137,
"context": " layer.handle_request(req, res, next);"
},
"filename": "node_modules/express/lib/router/route.js",
"absPath": "/app/node_modules/express/lib/router/route.js"
},
{
"filename": "node_modules/express/lib/router/route.js",
"absPath": "/app/node_modules/express/lib/router/route.js",
"function": "dispatch",
"libraryFrame": true,
"context": {
"pre": [
" var method = req.method.toLowerCase();",
" if (method === 'head' && !this.methods['head']) {",
" method = 'get';",
" }",
"",
" req.route = this;",
""
],
"post": [
"",
" function next(err) {",
" // signal to exit route",
" if (err && err === 'route') {",
" return done();",
" }",
""
]
},
"line": { "context": " next();", "number": 112 }
},
{
"filename": "node_modules/express/lib/router/layer.js",
"absPath": "/app/node_modules/express/lib/router/layer.js",
"function": "handle",
"libraryFrame": true,
"context": {
"pre": [
"",
" if (fn.length > 3) {",
" // not a standard request handler",
" return next();",
" }",
"",
" try {"
],
"post": [
" } catch (err) {",
" next(err);",
" }",
"};",
"",
"/**",
" * Check if this route matches `path`, if so"
]
},
"line": { "number": 95, "context": " fn(req, res, next);" }
},
{
"context": {
"pre": [
" // this should be done for the layer",
" self.process_params(layer, paramcalled, req, res, function (err) {",
" if (err) {",
" return next(layerError || err);",
" }",
"",
" if (route) {"
],
"post": [
" }",
"",
" trim_prefix(layer, layerError, layerPath, path);",
" });",
" }",
"",
" function trim_prefix(layer, layerError, layerPath, path) {"
]
},
"line": {
"number": 281,
"context":
" return layer.handle_request(req, res, next);"
},
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "<anonymous>",
"libraryFrame": true
},
{
"line": { "number": 335, "context": " return done();" },
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "process_params",
"libraryFrame": true,
"context": {
"post": [
" }",
"",
" var i = 0;",
" var name;",
" var paramIndex = 0;",
" var key;",
" var paramVal;"
],
"pre": [
" var params = this.params;",
"",
" // captured parameters from the layer, keys and values",
" var keys = layer.keys;",
"",
" // fast track",
" if (!keys || keys.length === 0) {"
]
}
},
{
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "next",
"libraryFrame": true,
"context": {
"pre": [
" // Capture one-time layer values",
" req.params = self.mergeParams",
" ? mergeParams(layer.params, parentParams)",
" : layer.params;",
" var layerPath = layer.path;",
"",
" // this should be done for the layer"
],
"post": [
" if (err) {",
" return next(layerError || err);",
" }",
"",
" if (route) {",
" return layer.handle_request(req, res, next);",
" }"
]
},
"line": {
"number": 275,
"context":
" self.process_params(layer, paramcalled, req, res, function (err) {"
}
},
{
"line": { "context": " next();", "number": 174 },
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "handle",
"libraryFrame": true,
"context": {
"pre": [
" });",
" }",
"",
" // setup basic req values",
" req.baseUrl = parentUrl;",
" req.originalUrl = req.originalUrl || req.url;",
""
],
"post": [
"",
" function next(err) {",
" var layerError = err === 'route'",
" ? null",
" : err;",
"",
" // remove added slash"
]
}
},
{
"libraryFrame": true,
"context": {
"pre": [
" * @public",
" */",
"",
"var proto = module.exports = function(options) {",
" var opts = options || {};",
"",
" function router(req, res, next) {"
],
"post": [
" }",
"",
" // mixin Router class functions",
" setPrototypeOf(router, proto)",
"",
" router.params = {};",
" router._params = [];"
]
},
"line": {
"number": 47,
"context": " router.handle(req, res, next);"
},
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "router"
},
{
"filename": "node_modules/express/lib/router/layer.js",
"absPath": "/app/node_modules/express/lib/router/layer.js",
"function": "handle",
"libraryFrame": true,
"context": {
"pre": [
"",
" if (fn.length > 3) {",
" // not a standard request handler",
" return next();",
" }",
"",
" try {"
],
"post": [
" } catch (err) {",
" next(err);",
" }",
"};",
"",
"/**",
" * Check if this route matches `path`, if so"
]
},
"line": { "number": 95, "context": " fn(req, res, next);" }
},
{
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "trim_prefix",
"libraryFrame": true,
"context": {
"pre": [
" }",
"",
" debug('%s %s : %s', layer.name, layerPath, req.originalUrl);",
"",
" if (layerError) {",
" layer.handle_error(layerError, req, res, next);",
" } else {"
],
"post": [
" }",
" }",
"};",
"",
"/**",
" * Process any parameters for the layer.",
" * @private"
]
},
"line": {
"number": 317,
"context": " layer.handle_request(req, res, next);"
},
"filename": "node_modules/express/lib/router/index.js"
},
{
"libraryFrame": true,
"context": {
"pre": [
" return next(layerError || err);",
" }",
"",
" if (route) {",
" return layer.handle_request(req, res, next);",
" }",
""
],
"post": [
" });",
" }",
"",
" function trim_prefix(layer, layerError, layerPath, path) {",
" if (layerPath.length !== 0) {",
" // Validate path breaks on a path separator",
" var c = path[layerPath.length]"
]
},
"line": {
"number": 284,
"context":
" trim_prefix(layer, layerError, layerPath, path);"
},
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "<anonymous>"
},
{
"function": "process_params",
"libraryFrame": true,
"context": {
"pre": [
" var params = this.params;",
"",
" // captured parameters from the layer, keys and values",
" var keys = layer.keys;",
"",
" // fast track",
" if (!keys || keys.length === 0) {"
],
"post": [
" }",
"",
" var i = 0;",
" var name;",
" var paramIndex = 0;",
" var key;",
" var paramVal;"
]
},
"line": { "number": 335, "context": " return done();" },
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js"
},
{
"line": {
"number": 275,
"context":
" self.process_params(layer, paramcalled, req, res, function (err) {"
},
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "next",
"libraryFrame": true,
"context": {
"pre": [
" // Capture one-time layer values",
" req.params = self.mergeParams",
" ? mergeParams(layer.params, parentParams)",
" : layer.params;",
" var layerPath = layer.path;",
"",
" // this should be done for the layer"
],
"post": [
" if (err) {",
" return next(layerError || err);",
" }",
"",
" if (route) {",
" return layer.handle_request(req, res, next);",
" }"
]
}
},
{
"line": { "number": 27, "context": " next()" },
"filename": "server.js",
"absPath": "/app/server.js",
"function": "<anonymous>",
"libraryFrame": false,
"context": {
"post": [
"})",
"",
"app.use(require('./server/coffee'))",
"app.use('/api', require('./server/routes'))",
"app.get('*', function (req, res) {",
" res.sendFile(path.resolve(__dirname, 'client/build', 'index.html'))",
"})"
],
"pre": [
"app.use(require('body-parser').json())",
"app.use(express.static('client/build'))",
"app.use(function (req, res, next) {",
" apm.setTag('foo', 'bar')",
" apm.setTag('lorem', 'ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.')",
" apm.setTag('this-is-a-very-long-tag-name-without-any-spaces', 'test')",
" apm.setTag('multi-line', 'foo\\nbar\\nbaz')"
]
}
},
{
"function": "handle",
"libraryFrame": true,
"context": {
"post": [
" } catch (err) {",
" next(err);",
" }",
"};",
"",
"/**",
" * Check if this route matches `path`, if so"
],
"pre": [
"",
" if (fn.length > 3) {",
" // not a standard request handler",
" return next();",
" }",
"",
" try {"
]
},
"line": { "context": " fn(req, res, next);", "number": 95 },
"filename": "node_modules/express/lib/router/layer.js",
"absPath": "/app/node_modules/express/lib/router/layer.js"
},
{
"libraryFrame": true,
"context": {
"pre": [
" }",
"",
" debug('%s %s : %s', layer.name, layerPath, req.originalUrl);",
"",
" if (layerError) {",
" layer.handle_error(layerError, req, res, next);",
" } else {"
],
"post": [
" }",
" }",
"};",
"",
"/**",
" * Process any parameters for the layer.",
" * @private"
]
},
"line": {
"number": 317,
"context": " layer.handle_request(req, res, next);"
},
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "trim_prefix"
},
{
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "<anonymous>",
"libraryFrame": true,
"context": {
"pre": [
" return next(layerError || err);",
" }",
"",
" if (route) {",
" return layer.handle_request(req, res, next);",
" }",
""
],
"post": [
" });",
" }",
"",
" function trim_prefix(layer, layerError, layerPath, path) {",
" if (layerPath.length !== 0) {",
" // Validate path breaks on a path separator",
" var c = path[layerPath.length]"
]
},
"line": {
"number": 284,
"context":
" trim_prefix(layer, layerError, layerPath, path);"
}
},
{
"libraryFrame": true,
"context": {
"post": [
" }",
"",
" var i = 0;",
" var name;",
" var paramIndex = 0;",
" var key;",
" var paramVal;"
],
"pre": [
" var params = this.params;",
"",
" // captured parameters from the layer, keys and values",
" var keys = layer.keys;",
"",
" // fast track",
" if (!keys || keys.length === 0) {"
]
},
"line": { "number": 335, "context": " return done();" },
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "process_params"
},
{
"libraryFrame": true,
"context": {
"pre": [
" // Capture one-time layer values",
" req.params = self.mergeParams",
" ? mergeParams(layer.params, parentParams)",
" : layer.params;",
" var layerPath = layer.path;",
"",
" // this should be done for the layer"
],
"post": [
" if (err) {",
" return next(layerError || err);",
" }",
"",
" if (route) {",
" return layer.handle_request(req, res, next);",
" }"
]
},
"line": {
"number": 275,
"context":
" self.process_params(layer, paramcalled, req, res, function (err) {"
},
"filename": "node_modules/express/lib/router/index.js",
"absPath": "/app/node_modules/express/lib/router/index.js",
"function": "next"
},
{
"absPath":
"/app/node_modules/elastic-apm-node/lib/instrumentation/modules/express.js",
"function": "nextHook",
"libraryFrame": true,
"context": {
"pre": [
" return function serveStatic (req, res, next) {",
" req._elastic_apm_static = true",
"",
" return origServeStatic(req, res, nextHook)",
"",
" function nextHook (err) {",
" if (!err) req._elastic_apm_static = false"
],
"post": [
" }",
" }",
" }",
" })",
"",
" return express",
"}"
]
},
"line": {
"number": 90,
"context": " return next.apply(this, arguments)"
},
"filename":
"node_modules/elastic-apm-node/lib/instrumentation/modules/express.js"
},
{
"filename": "node_modules/serve-static/index.js",
"absPath": "/app/node_modules/serve-static/index.js",
"function": "error",
"libraryFrame": true,
"context": {
"post": [
" })",
"",
" // pipe",
" stream.pipe(res)",
" }",
"}",
""
],
"pre": [
" // forward errors",
" stream.on('error', function error (err) {",
" if (forwardError || !(err.statusCode < 500)) {",
" next(err)",
" return",
" }",
""
]
},
"line": { "number": 121, "context": " next()" }
},
{
"filename": "events.js",
"absPath": "events.js",
"function": "emitOne",
"libraryFrame": true,
"line": { "number": 96 }
},
{
"filename": "events.js",
"absPath": "events.js",
"function": "emit",
"libraryFrame": true,
"line": { "number": 188 }
},
{
"libraryFrame": true,
"context": {
"pre": [
" * @param {Error} [err]",
" * @private",
" */",
"",
"SendStream.prototype.error = function error (status, err) {",
" // emit if listeners instead of responding",
" if (hasListeners(this, 'error')) {"
],
"post": [
" expose: false",
" }))",
" }",
"",
" var res = this.res",
" var msg = statuses[status] || String(status)",
" var doc = createHtmlDocument('Error', escapeHtml(msg))"
]
},
"line": {
"number": 270,
"context":
" return this.emit('error', createError(status, err, {"
},
"filename": "node_modules/send/index.js",
"absPath": "/app/node_modules/send/index.js",
"function": "error"
},
{
"function": "onStatError",
"libraryFrame": true,
"context": {
"pre": [
" */",
"",
"SendStream.prototype.onStatError = function onStatError (error) {",
" switch (error.code) {",
" case 'ENAMETOOLONG':",
" case 'ENOENT':",
" case 'ENOTDIR':"
],
"post": [
" break",
" default:",
" this.error(500, error)",
" break",
" }",
"}",
""
]
},
"line": {
"number": 421,
"context": " this.error(404, error)"
},
"filename": "node_modules/send/index.js",
"absPath": "/app/node_modules/send/index.js"
},
{
"line": {
"number": 736,
"context": " ? self.onStatError(err)"
},
"filename": "node_modules/send/index.js",
"absPath": "/app/node_modules/send/index.js",
"function": "next",
"libraryFrame": true,
"context": {
"pre": [
" self.emit('file', path, stat)",
" self.send(path, stat)",
" })",
"",
" function next (err) {",
" if (self._extensions.length <= i) {",
" return err"
],
"post": [
" : self.error(404)",
" }",
"",
" var p = path + '.' + self._extensions[i++]",
"",
" debug('stat \"%s\"', p)",
" fs.stat(p, function (err, stat) {"
]
}
},
{
"filename": "node_modules/send/index.js",
"absPath": "/app/node_modules/send/index.js",
"function": "onstat",
"libraryFrame": true,
"context": {
"pre": [
" var i = 0",
" var self = this",
"",
" debug('stat \"%s\"', path)",
" fs.stat(path, function onstat (err, stat) {",
" if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {",
" // not found, check extensions"
],
"post": [
" }",
" if (err) return self.onStatError(err)",
" if (stat.isDirectory()) return self.redirect(path)",
" self.emit('file', path, stat)",
" self.send(path, stat)",
" })",
""
]
},
"line": { "number": 725, "context": " return next(err)" }
},
{
"context": {
"pre": [
" var trans = this.currentTransaction",
"",
" return elasticAPMCallbackWrapper",
"",
" function elasticAPMCallbackWrapper () {",
" var prev = ins.currentTransaction",
" ins.currentTransaction = trans"
],
"post": [
" ins.currentTransaction = prev",
" return result",
" }",
"}",
"",
"Instrumentation.prototype._recoverTransaction = function (trans) {",
" if (this.currentTransaction === trans) return"
]
},
"line": {
"number": 116,
"context": " var result = original.apply(this, arguments)"
},
"filename":
"node_modules/elastic-apm-node/lib/instrumentation/index.js",
"absPath":
"/app/node_modules/elastic-apm-node/lib/instrumentation/index.js",
"function": "elasticAPMCallbackWrapper",
"libraryFrame": true
},
{
"filename": "fs.js",
"absPath": "fs.js",
"function": "FSReqWrap.oncomplete",
"libraryFrame": true,
"line": { "number": 123 }
}
],
"message": "Cannot read property 'level' of undefined"
},
"groupingKey": "c00e245c2fbebaf178fc31eeb2bb0250"
},
"context": {
"request": {
"http_version": "1.1",
"method": "GET",
"url": {
"raw": "/is-it-coffee-time",
"hostname": "opbeans-node",
"port": "3000",
"pathname": "/is-it-coffee-time"
},
"socket": {
"remote_address": "::ffff:172.19.0.7",
"encrypted": false
},
"headers": {
"user-agent": "workload/2.4.3",
"host": "opbeans-node:3000",
"connection": "close"
}
},
"response": {
"status_code": 200,
"headers": {},
"headers_sent": false,
"finished": false
},
"system": {
"architecture": "x64",
"platform": "linux",
"hostname": "b4cb1df7e088"
},
"tags": {
"foo": "bar",
"lorem":
"ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.",
"this-is-a-very-long-tag-name-without-any-spaces": "test",
"multi-line": "foo\nbar\nbaz"
},
"custom": {},
"service": {
"language": { "name": "javascript" },
"runtime": { "name": "node", "version": "v6.12.0" },
"framework": { "name": "express", "version": "4.16.2" },
"name": "opbeans-node",
"agent": { "name": "nodejs", "version": "0.9.0" }
},
"process": {
"pid": 36,
"title": "node /app/server.js",
"argv": [
"/usr/local/bin/node",
"/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js"
]
},
"user": {}
},
"beat": {
"name": "8be8857dbeda",
"hostname": "8be8857dbeda",
"version": "7.0.0-alpha1"
}
},
"occurrencesCount": 18
},
"status": "SUCCESS"
},
"urlParams": {
"page": 0,
"serviceName": "opbeans-node",
"transactionType": "request",
"errorGroupId": "c00e245c2fbebaf178fc31eeb2bb0250",
"start": "2018-01-09T14:24:42.561Z",
"end": "2018-01-09T14:39:42.561Z"
}
}

View file

@ -1,225 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import {
unit,
units,
px,
colors,
borderRadius
} from '../../../../style/variables';
import { get, capitalize, isEmpty } from 'lodash';
import { STATUS } from '../../../../constants';
import { StickyProperties } from '../../../shared/StickyProperties';
import { Tab, HeaderMedium } from '../../../shared/UIComponents';
import { DiscoverErrorButton } from '../../../shared/DiscoverButtons/DiscoverErrorButton';
import {
PropertiesTable,
getPropertyTabNames
} from '../../../shared/PropertiesTable';
import { Stacktrace } from '../../../shared/Stacktrace';
import {
SERVICE_AGENT_NAME,
SERVICE_LANGUAGE_NAME,
USER_ID,
REQUEST_URL_FULL,
REQUEST_METHOD,
ERROR_EXC_HANDLED,
ERROR_LOG_STACKTRACE,
ERROR_EXC_STACKTRACE
} from '../../../../../common/constants';
import { fromQuery, toQuery, history } from '../../../../utils/url';
const Container = styled.div`
position: relative;
border: 1px solid ${colors.gray4};
border-radius: ${borderRadius};
margin-top: ${px(units.plus)};
`;
const HeaderContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: ${px(units.plus)} ${px(units.plus)} 0;
margin-bottom: ${px(unit)};
`;
const TabContainer = styled.div`
padding: 0 ${px(units.plus)};
border-bottom: 1px solid ${colors.gray4};
`;
const TabContentContainer = styled.div`
padding: ${px(units.plus)} ${px(units.plus)} 0;
`;
const EXC_STACKTRACE_TAB = 'exception_stacktrace';
const LOG_STACKTRACE_TAB = 'log_stacktrace';
// Ensure the selected tab exists or use the first
function getCurrentTab(tabs = [], selectedTab) {
return tabs.includes(selectedTab) ? selectedTab : tabs[0];
}
function getTabs(context, logStackframes) {
const dynamicProps = Object.keys(context);
return [
...(logStackframes ? [LOG_STACKTRACE_TAB] : []),
EXC_STACKTRACE_TAB,
...getPropertyTabNames(dynamicProps)
];
}
function DetailView({ errorGroup, urlParams, location }) {
if (errorGroup.status !== STATUS.SUCCESS) {
return null;
}
if (isEmpty(errorGroup.data.error)) {
return null;
}
const stickyProperties = [
{
fieldName: '@timestamp',
label: 'Timestamp',
val: get(errorGroup.data.error, '@timestamp'),
width: '50%'
},
{
fieldName: REQUEST_URL_FULL,
label: 'URL',
val: get(errorGroup.data.error, REQUEST_URL_FULL, 'N/A'),
truncated: true,
width: '50%'
},
{
fieldName: REQUEST_METHOD,
label: 'Request method',
val: get(errorGroup.data.error, REQUEST_METHOD, 'N/A'),
width: '25%'
},
{
fieldName: ERROR_EXC_HANDLED,
label: 'Handled',
val: get(errorGroup.data.error, ERROR_EXC_HANDLED, 'N/A'),
width: '25%'
},
{
fieldName: USER_ID,
label: 'User ID',
val: get(errorGroup.data.error, USER_ID, 'N/A'),
width: '50%'
}
];
const excStackframes = get(errorGroup.data.error, ERROR_EXC_STACKTRACE);
const logStackframes = get(errorGroup.data.error, ERROR_LOG_STACKTRACE);
const codeLanguage = get(errorGroup.data.error, SERVICE_LANGUAGE_NAME);
const context = get(errorGroup.data.error, 'context', {});
const tabs = getTabs(context, logStackframes);
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
const occurencesCount = errorGroup.data.occurrencesCount;
const agentName = get(errorGroup.data.error, SERVICE_AGENT_NAME);
return (
<Container>
<HeaderContainer>
<HeaderMedium
css={`
margin: 0;
`}
>
Error occurrence
</HeaderMedium>
<DiscoverErrorButton
error={errorGroup.data.error}
kuery={urlParams.kuery}
>
<EuiButtonEmpty iconType="discoverApp">
{`View ${occurencesCount} occurences in Discover`}
</EuiButtonEmpty>
</DiscoverErrorButton>
</HeaderContainer>
<TabContentContainer>
<StickyProperties stickyProperties={stickyProperties} />
</TabContentContainer>
<TabContainer>
{tabs.map(key => {
return (
<Tab
onClick={() => {
history.replace({
...location,
search: fromQuery({
...toQuery(location.search),
detailTab: key
})
});
}}
selected={currentTab === key}
key={key}
>
{capitalize(key.replace('_', ' '))}
</Tab>
);
})}
</TabContainer>
<TabContentContainer>
<TabContent
currentTab={currentTab}
logStackframes={logStackframes}
excStackframes={excStackframes}
codeLanguage={codeLanguage}
errorGroup={errorGroup}
agentName={agentName}
/>
</TabContentContainer>
</Container>
);
}
function TabContent({
currentTab,
logStackframes,
excStackframes,
codeLanguage,
errorGroup,
agentName
}) {
switch (currentTab) {
case LOG_STACKTRACE_TAB:
return (
<Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />
);
case EXC_STACKTRACE_TAB:
return (
<Stacktrace stackframes={excStackframes} codeLanguage={codeLanguage} />
);
default:
return (
<PropertiesTable
propData={errorGroup.data.error.context[currentTab]}
propKey={currentTab}
agentName={agentName}
/>
);
}
}
DetailView.propTypes = {
location: PropTypes.object.isRequired
};
export default DetailView;

View file

@ -0,0 +1,257 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButtonEmpty,
EuiSpacer,
EuiTab,
EuiTabs,
EuiTitle
} from '@elastic/eui';
import { capitalize, get } from 'lodash';
import React from 'react';
import { RRRRenderResponse } from 'react-redux-request';
import styled from 'styled-components';
import {
ERROR_EXC_STACKTRACE,
ERROR_LOG_STACKTRACE
} from 'x-pack/plugins/apm/common/constants';
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import {
ERROR_EXC_HANDLED,
REQUEST_METHOD,
REQUEST_URL_FULL,
TRACE_ID,
TRANSACTION_ID,
USER_ID
} from '../../../../../common/constants';
import { STATUS } from '../../../../constants';
import {
borderRadius,
colors,
px,
unit,
units
} from '../../../../style/variables';
import { fromQuery, history, toQuery } from '../../../../utils/url';
import { KibanaLink, legacyEncodeURIComponent } from '../../../../utils/url';
import { DiscoverErrorButton } from '../../../shared/DiscoverButtons/DiscoverErrorButton';
import {
getPropertyTabNames,
PropertiesTable
} from '../../../shared/PropertiesTable';
import { Stacktrace } from '../../../shared/Stacktrace';
import { StickyProperties } from '../../../shared/StickyProperties';
const Container = styled.div`
position: relative;
border: 1px solid ${colors.gray4};
border-radius: ${borderRadius};
margin-top: ${px(units.plus)};
`;
const HeaderContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: ${px(units.plus)} ${px(units.plus)} 0;
margin-bottom: ${px(unit)};
`;
const PaddedContainer = styled.div`
padding: ${px(units.plus)} ${px(units.plus)} 0;
`;
const EXC_STACKTRACE_TAB = 'exception_stacktrace';
const LOG_STACKTRACE_TAB = 'log_stacktrace';
interface Props {
errorGroup: RRRRenderResponse<ErrorGroupAPIResponse>;
urlParams: IUrlParams;
location: any;
}
export function DetailView({ errorGroup, urlParams, location }: Props) {
if (errorGroup.status !== STATUS.SUCCESS) {
return null;
}
const { transaction, error, occurrencesCount } = errorGroup.data;
if (!error) {
return null;
}
const transactionLink = getTransactionLink(error, transaction);
const stickyProperties = [
{
fieldName: '@timestamp',
label: 'Timestamp',
val: error['@timestamp'],
width: '50%'
},
{
fieldName: REQUEST_URL_FULL,
label: 'URL',
val: get(error, REQUEST_URL_FULL, 'N/A'),
truncated: true,
width: '50%'
},
{
fieldName: REQUEST_METHOD,
label: 'Request method',
val: get(error, REQUEST_METHOD, 'N/A'),
width: '25%'
},
{
fieldName: ERROR_EXC_HANDLED,
label: 'Handled',
val: get(error, ERROR_EXC_HANDLED, 'N/A'),
width: '25%'
},
{
fieldName: TRANSACTION_ID,
label: 'Transaction sample ID',
val: transactionLink || 'N/A',
width: '25%'
},
{
fieldName: USER_ID,
label: 'User ID',
val: get(error, USER_ID, 'N/A'),
width: '25%'
}
];
const tabs = getTabs(error);
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
return (
<Container>
<HeaderContainer>
<EuiTitle size="s">
<h3>Error occurence</h3>
</EuiTitle>
<DiscoverErrorButton error={error} kuery={urlParams.kuery}>
<EuiButtonEmpty iconType="discoverApp">
{`View ${occurrencesCount} occurences in Discover`}
</EuiButtonEmpty>
</DiscoverErrorButton>
</HeaderContainer>
<PaddedContainer>
<StickyProperties stickyProperties={stickyProperties} />
</PaddedContainer>
<EuiSpacer />
<EuiTabs>
{tabs.map(key => {
return (
<EuiTab
onClick={() => {
history.replace({
...location,
search: fromQuery({
...toQuery(location.search),
detailTab: key
})
});
}}
isSelected={currentTab === key}
key={key}
>
{capitalize(key.replace('_', ' '))}
</EuiTab>
);
})}
</EuiTabs>
<PaddedContainer>
<TabContent error={error} currentTab={currentTab} />
</PaddedContainer>
</Container>
);
}
function getTransactionLink(error: APMError, transaction?: Transaction) {
if (!transaction || !get(error, 'transaction.sampled')) {
return;
}
const path = `/${
transaction.context.service.name
}/transactions/${legacyEncodeURIComponent(
transaction.transaction.type
)}/${legacyEncodeURIComponent(transaction.transaction.name)}`;
return (
<KibanaLink
pathname={'/app/apm'}
hash={path}
query={{
transactionId: transaction.transaction.id,
traceid: get(transaction, TRACE_ID)
}}
>
{transaction.transaction.id}
</KibanaLink>
);
}
type MaybeStackframes = IStackframe[] | undefined;
export function TabContent({
error,
currentTab
}: {
error: APMError;
currentTab?: string;
}) {
const codeLanguage = error.context.service.name;
const agentName = error.context.service.agent.name;
const excStackframes: MaybeStackframes = get(error, ERROR_EXC_STACKTRACE);
const logStackframes: MaybeStackframes = get(error, ERROR_LOG_STACKTRACE);
switch (currentTab) {
case LOG_STACKTRACE_TAB:
case undefined:
return (
<Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />
);
case EXC_STACKTRACE_TAB:
return (
<Stacktrace stackframes={excStackframes} codeLanguage={codeLanguage} />
);
default:
const propData = error.context[currentTab] as any;
return (
<PropertiesTable
propData={propData}
propKey={currentTab}
agentName={agentName}
/>
);
}
}
// Ensure the selected tab exists or use the first
export function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
return tabs.includes(selectedTab!) ? selectedTab : tabs[0];
}
export function getTabs(error: APMError) {
const hasLogStacktrace = get(error, ERROR_LOG_STACKTRACE, []).length > 0;
const contextKeys = Object.keys(error.context);
return [
...(hasLogStacktrace ? [LOG_STACKTRACE_TAB] : []),
EXC_STACKTRACE_TAB,
...getPropertyTabNames(contextKeys)
];
}

View file

@ -5,7 +5,7 @@
*/
import { connect } from 'react-redux';
import ErrorGroupDetails from './view';
import { ErrorGroupDetails } from './view';
import { getUrlParams } from '../../../store/urlParams';
function mapStateToProps(state = {}) {

View file

@ -4,32 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { get } from 'lodash';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { HeaderLarge } from '../../shared/UIComponents';
import DetailView from './DetailView';
import Distribution from './Distribution';
import { KueryBar } from '../../shared/KueryBar';
import { EuiText, EuiBadge, EuiSpacer } from '@elastic/eui';
import {
unit,
units,
px,
colors,
fontFamilyCode,
fontSizes
} from '../../../style/variables';
import {
ERROR_CULPRIT,
ERROR_LOG_MESSAGE,
ERROR_EXC_HANDLED,
ERROR_EXC_MESSAGE,
ERROR_EXC_HANDLED
ERROR_LOG_MESSAGE
} from '../../../../common/constants';
import { ErrorDistributionRequest } from '../../../store/reactReduxRequest/errorDistribution';
import { ErrorGroupDetailsRequest } from '../../../store/reactReduxRequest/errorGroup';
import { IUrlParams } from '../../../store/urlParams';
import {
colors,
fontFamilyCode,
fontSizes,
px,
unit,
units
} from '../../../style/variables';
// @ts-ignore
import { KueryBar } from '../../shared/KueryBar';
import { DetailView } from './DetailView';
// @ts-ignore
import Distribution from './Distribution';
const Titles = styled.div`
margin-bottom: ${px(units.plus)};
@ -57,7 +57,7 @@ const Culprit = styled.div`
font-family: ${fontFamilyCode};
`;
function getShortGroupId(errorGroupId) {
function getShortGroupId(errorGroupId?: string) {
if (!errorGroupId) {
return 'N/A';
}
@ -65,7 +65,12 @@ function getShortGroupId(errorGroupId) {
return errorGroupId.slice(0, 5);
}
function ErrorGroupDetails({ urlParams, location }) {
interface Props {
urlParams: IUrlParams;
location: any;
}
export function ErrorGroupDetails({ urlParams, location }: Props) {
return (
<ErrorGroupDetailsRequest
urlParams={urlParams}
@ -80,12 +85,16 @@ function ErrorGroupDetails({ urlParams, location }) {
return (
<div>
<HeaderLarge>
Error group {getShortGroupId(urlParams.errorGroupId)}
{isUnhandled && (
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
)}
</HeaderLarge>
<EuiTitle>
<span>
Error group {getShortGroupId(urlParams.errorGroupId)}
{isUnhandled && (
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
)}
</span>
</EuiTitle>
<EuiSpacer size="m" />
<KueryBar />
@ -124,9 +133,3 @@ function ErrorGroupDetails({ urlParams, location }) {
/>
);
}
ErrorGroupDetails.propTypes = {
location: PropTypes.object.isRequired
};
export default ErrorGroupDetails;

View file

@ -4,13 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiSpacer,
// @ts-ignore
EuiTab,
// @ts-ignore
EuiTabs
} from '@elastic/eui';
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { capitalize, first, get } from 'lodash';
import React from 'react';
import styled from 'styled-components';

View file

@ -12,7 +12,7 @@ import {
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { DiscoverButton } from './DiscoverButton';
function getDiscoverQuery(error: APMError, kuery: string) {
function getDiscoverQuery(error: APMError, kuery?: string) {
const serviceName = error.context.service.name;
const groupId = error.error.grouping_key;
let query = `${SERVICE_NAME}:"${serviceName}" AND ${ERROR_GROUP_ID}:"${groupId}"`;
@ -32,9 +32,9 @@ function getDiscoverQuery(error: APMError, kuery: string) {
};
}
export const DiscoverErrorButton: React.SFC<{
const DiscoverErrorButton: React.SFC<{
readonly error: APMError;
readonly kuery: string;
readonly kuery?: string;
}> = ({ error, kuery, children }) => {
return (
<DiscoverButton
@ -43,3 +43,5 @@ export const DiscoverErrorButton: React.SFC<{
/>
);
};
export { DiscoverErrorButton };

View file

@ -20,7 +20,7 @@ import {
} from '../../../style/variables';
export interface IStickyProperty {
val: any;
val: React.ReactNode | Date;
label: string;
fieldName?: string;
width?: 0 | string;
@ -90,7 +90,7 @@ function getPropertyValue({
truncated = false
}: Partial<IStickyProperty>) {
if (fieldName === '@timestamp') {
return <TimestampValue timestamp={val} />;
return <TimestampValue timestamp={val as Date} />;
}
if (truncated) {

View file

@ -1,52 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import styled from 'styled-components';
import { colors, fontSizes, px, unit, units } from '../../style/variables';
export const HeaderContainer = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: ${px(units.plus)};
h1 {
font-size: ${fontSizes.xxlarge};
}
`;
export const HeaderLarge = styled.h1`
font-size: ${fontSizes.xxlarge};
margin-bottom: ${px(units.plus)};
&:after {
content: '.';
visibility: hidden;
}
`;
export const HeaderMedium = styled<{ css: string }, 'h2'>('h2')`
margin: ${px(units.plus)} 0;
font-size: ${fontSizes.xlarge};
${props => props.css};
`;
export const HeaderSmall = styled<{ css: string }, 'h3'>('h3')`
margin: ${px(units.plus)} 0;
font-size: ${fontSizes.large};
${props => props.css};
`;
export const Tab = styled<{ selected: boolean }, 'div'>('div')`
display: inline-block;
font-size: ${fontSizes.large};
padding: ${px(unit)} ${px(unit + units.quarter)};
text-align: center;
cursor: pointer;
user-select: none;
border-bottom: ${props =>
props.selected && `${units.quarter / 2}px solid ${colors.blue1}`};
`;

View file

@ -5,16 +5,25 @@
*/
import { ESFilter } from 'elasticsearch';
import { get } from 'lodash';
import { oc } from 'ts-optchain';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { ERROR_GROUP_ID, SERVICE_NAME } from '../../../common/constants';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import {
ERROR_GROUP_ID,
SERVICE_NAME,
TRANSACTION_SAMPLED
} from '../../../common/constants';
import { Setup } from '../helpers/setup_request';
import { getTransaction } from '../transactions/get_transaction';
export interface ErrorGroupAPIResponse {
transaction?: Transaction;
error?: APMError;
occurrencesCount?: number;
}
// TODO: rename from "getErrorGroup" to "getErrorGroupSample" (since a single error is returned, not an errorGroup)
export async function getErrorGroup({
serviceName,
groupId,
@ -49,21 +58,30 @@ export async function getErrorGroup({
size: 1,
query: {
bool: {
filter
filter,
should: [{ term: { [TRANSACTION_SAMPLED]: true } }]
}
},
sort: [
{
'@timestamp': 'desc'
}
{ _score: 'desc' }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top
{ '@timestamp': { order: 'desc' } } // sort by timestamp to get the most recent error
]
}
};
const resp = await client<APMError>('search', params);
const error = oc(resp).hits.hits[0]._source();
const transactionId = oc(error).transaction.id();
const traceId: string | undefined = get(error, 'trace.id'); // cannot use oc because 'trace' doesn't exist on v1 errors
let transaction;
if (transactionId) {
transaction = await getTransaction(transactionId, traceId, setup);
}
return {
error: oc(resp).hits.hits[0]._source(),
transaction,
error,
occurrencesCount: oc(resp).hits.total()
};
}

View file

@ -14,7 +14,7 @@ import {
} from '../../../../common/constants';
import { Setup } from '../../helpers/setup_request';
export type TransactionAPIResponse = Transaction | null;
export type TransactionAPIResponse = Transaction | undefined;
export async function getTransaction(
transactionId: string,
@ -56,5 +56,5 @@ export async function getTransaction(
}
const resp = await client<Transaction>('search', params);
return oc(resp).hits.hits[0]._source() || null;
return oc(resp).hits.hits[0]._source();
}

View file

@ -76,6 +76,7 @@ interface ErrorV2 extends APMDocV2 {
context: Context;
transaction: {
id: string; // transaction ID is required in v2
sampled?: boolean;
};
error: {
id: string; // ID is required in v2