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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

/**
* Generates OTel traces with no service.environment (namespace omitted).
* Use to verify UI does not crash when opening error samples, transaction/span
* flyouts, or agent config links for data without environment.
*/

import type { ApmOtelFields, OtelInstance } from '@kbn/synthtrace-client';
import { apm, ApmSynthtracePipelineSchema } from '@kbn/synthtrace-client';
import { withClient } from '../lib/utils/with_client';
import type { Scenario } from '../cli/scenario';

const scenario: Scenario<ApmOtelFields> = async (runOptions) => {
return {
generate: ({ range, clients: { apmEsClient } }) => {
const transactionName = 'oteldemo.AdServiceSynth/GetAds';

const { logger } = runOptions;

const edotInstance = apm
.otelService({
name: 'adservice-edot-synth',
sdkLanguage: 'java',
sdkName: 'opentelemetry',
distro: 'elastic',
})
.instance('edot-instance');

const otelNativeInstance = apm
.otelService({
name: 'sendotlp-otel-native-synth',
sdkName: 'otlp',
sdkLanguage: 'nodejs',
})
.instance('otel-native-instance');

const successfulTimestamps = range.interval('1m').rate(180);
const failedTimestamps = range.interval('1m').rate(40);

const instanceSpans = (instance: OtelInstance) => {
const successfulTraceEvents = successfulTimestamps.generator((timestamp) =>
instance
.span({
name: transactionName,
kind: 'Server',
})
.timestamp(timestamp)
.duration(1000)
.success()
.children(
instance
.dbExitSpan({
name: 'GET apm-*/_search',
type: 'elasticsearch',
})
.duration(1000)
.success()
.timestamp(timestamp),
instance
.span({
name: 'custom_operation',
kind: 'Internal',
})
.duration(100)
.success()
.timestamp(timestamp)
)
);

const failedTraceEvents = failedTimestamps.generator((timestamp) =>
instance
.span({ name: transactionName, kind: 'Server' })
.timestamp(timestamp)
.duration(1000)
.failure()
.errors(
instance
.error({
message: '[ResponseError] index_not_found_exception',
type: 'ResponseError',
})
.timestamp(timestamp + 50)
)
);

return [successfulTraceEvents, failedTraceEvents];
};

return [
withClient(
apmEsClient,
logger.perf('generating_otel_trace', () =>
[otelNativeInstance, edotInstance].flatMap((instance) => instanceSpans(instance))
)
),
];
},
setupPipeline: ({ apmEsClient }) => {
apmEsClient.setPipeline(apmEsClient.resolvePipelineType(ApmSynthtracePipelineSchema.Otel));
},
};
};

export default scenario;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render } from '@testing-library/react';
import { ErrorSampleContextualInsight } from './error_sample_contextual_insight';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';

jest.mock('../../../../context/apm_plugin/use_apm_plugin_context');

const mockUseApmPluginContext = useApmPluginContext as jest.MockedFunction<
typeof useApmPluginContext
>;

describe('ErrorSampleContextualInsight', () => {
beforeEach(() => {
mockUseApmPluginContext.mockReturnValue({
observabilityAIAssistant: undefined,
} as any);
});

it('renders null when error is undefined (OTel / incomplete data)', () => {
const { container } = render(
<ErrorSampleContextualInsight transaction={{ transaction: { name: 'GET /api' } }} />
);
expect(container.firstChild).toBeNull();
});

it('renders null when error has no service (missing service.environment case)', () => {
const { container } = render(
<ErrorSampleContextualInsight
error={
{
error: { exception: [{ message: 'err' }] },
service: undefined,
} as any
}
/>
);
expect(container.firstChild).toBeNull();
});

it('does not throw when error.service is missing', () => {
expect(() => {
render(
<ErrorSampleContextualInsight
error={
{
error: { exception: [{ message: 'err' }] },
} as any
}
/>
);
}).not.toThrow();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export function ErrorSampleContextualInsight({
error,
transaction,
}: {
error: {
error?: {
[AT_TIMESTAMP]: string;
error: Pick<APMError['error'], 'log' | 'exception' | 'id'>;
service: {
service?: {
name: string;
environment?: string;
language?: {
Expand All @@ -45,11 +45,11 @@ export function ErrorSampleContextualInsight({
const [exceptionStacktrace, setExceptionStacktrace] = useState('');

const messages = useMemo<Message[] | undefined>(() => {
const serviceName = error.service.name;
const languageName = error.service.language?.name ?? '';
const runtimeName = error.service.runtime?.name ?? '';
const runtimeVersion = error.service.runtime?.version ?? '';
const transactionName = transaction?.transaction.name ?? '';
const serviceName = error?.service?.name ?? '';
const languageName = error?.service?.language?.name ?? '';
const runtimeName = error?.service?.runtime?.name ?? '';
const runtimeVersion = error?.service?.runtime?.version ?? '';
const transactionName = transaction?.transaction?.name ?? '';

return observabilityAIAssistant?.getContextualInsightMessages({
message: `I'm looking at an exception and trying to understand what it means`,
Expand Down Expand Up @@ -78,6 +78,10 @@ export function ErrorSampleContextualInsight({
});
}, [error, transaction, logStacktrace, exceptionStacktrace, observabilityAIAssistant]);

if (!error?.service) {
return null;
}

return observabilityAIAssistant?.ObservabilityAIAssistantContextualInsight && messages ? (
<>
<EuiFlexItem>
Expand All @@ -95,7 +99,7 @@ export function ErrorSampleContextualInsight({
}}
style={{ display: 'none' }}
>
{error.error.log?.message && (
{error?.error?.log?.message && (
<ErrorSampleDetailTabContent error={error} currentTab={logStacktraceTab} />
)}
</div>
Expand All @@ -105,7 +109,7 @@ export function ErrorSampleContextualInsight({
}}
style={{ display: 'none' }}
>
{error.error.exception?.length && (
{error?.error?.exception?.length && (
<ErrorSampleDetailTabContent error={error} currentTab={exceptionStacktraceTab} />
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ import { useTimeRange } from '../../../../hooks/use_time_range';
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
import { buildUrl } from '../../../../utils/build_url';
import { OpenInDiscover } from '../../../shared/links/discover_links/open_in_discover';
import {
ENVIRONMENT_NOT_DEFINED,
getEnvironmentLabel,
} from '../../../../../common/environment_filter_values';

const TransactionLinkName = styled.div`
margin-left: ${({ theme }) => theme.euiTheme.size.s};
Expand Down Expand Up @@ -158,7 +162,7 @@ export function ErrorSampleDetails({

const tabs = getTabs(error);
const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
const urlFromError = error.error.page?.url || error.url?.full;
const urlFromError = error?.error?.page?.url || error?.url?.full;
const urlFromTransaction = transaction?.transaction?.page?.url || transaction?.url?.full;
const errorOrTransactionUrl = error?.url ? error : transaction;
const errorOrTransactionHttp = error?.http ? error : transaction;
Expand All @@ -174,9 +178,12 @@ export function ErrorSampleDetails({
const method = errorOrTransactionHttp?.http?.request?.method;
const status = errorOrTransactionHttp?.http?.response?.status_code;
const userAgent = errorOrTransactionUserAgent;
const errorEnvironment = error.service.environment;
const serviceVersion = error.service.version;
const isUnhandled = error.error.exception?.[0]?.handled === false;
const errorEnvironment =
error?.service?.environment ??
transaction?.service?.environment ??
ENVIRONMENT_NOT_DEFINED.value;
const serviceVersion = error?.service?.version ?? transaction?.service?.version ?? undefined;
const isUnhandled = error?.error?.exception?.[0]?.handled === false;

return (
<EuiPanel hasBorder={true}>
Expand Down Expand Up @@ -215,7 +222,7 @@ export function ErrorSampleDetails({
rangeTo={rangeTo}
queryParams={{
kuery,
serviceName: error?.service.name,
serviceName: error?.service?.name,
errorGroupId: groupId,
}}
/>
Expand All @@ -231,7 +238,7 @@ export function ErrorSampleDetails({
<Summary
items={[
<Timestamp
timestamp={errorData ? error.timestamp.us / 1000 : 0}
timestamp={errorData && error ? (error.timestamp?.us ?? 0) / 1000 : 0}
renderMode="tooltip"
/>,
errorUrl ? (
Expand Down Expand Up @@ -266,17 +273,15 @@ export function ErrorSampleDetails({
</TransactionDetailLink>
</EuiToolTip>
),
errorEnvironment ? (
<EuiToolTip
content={i18n.translate('xpack.apm.errorSampleDetails.serviceEnvironment', {
defaultMessage: 'Environment',
})}
>
<EuiBadge color="hollow" tabIndex={0}>
{errorEnvironment}
</EuiBadge>
</EuiToolTip>
) : null,
<EuiToolTip
content={i18n.translate('xpack.apm.errorSampleDetails.serviceEnvironment', {
defaultMessage: 'Environment',
})}
>
<EuiBadge color="hollow" tabIndex={0}>
{getEnvironmentLabel(errorEnvironment)}
</EuiBadge>
</EuiToolTip>,
serviceVersion ? (
<EuiToolTip
content={i18n.translate('xpack.apm.errorSampleDetails.serviceVersion', {
Expand Down Expand Up @@ -311,8 +316,8 @@ export function ErrorSampleDetails({

{ErrorSampleAiInsight && error && (
<ErrorSampleAiInsight
errorId={error.error.id}
serviceName={error.service.name}
errorId={error?.error?.id}
serviceName={error?.service?.name ?? transaction?.service?.name}
start={start}
end={end}
environment={environment}
Expand Down Expand Up @@ -356,7 +361,7 @@ export function ErrorSampleDetailTabContent({
currentTab,
}: {
error: {
service: {
service?: {
language?: {
name?: string;
};
Expand All @@ -366,23 +371,23 @@ export function ErrorSampleDetailTabContent({
};
currentTab: ErrorTab;
}) {
const codeLanguage = error?.service.language?.name;
const exceptions = error?.error.exception || [];
const codeLanguage = error?.service?.language?.name;
const exceptions = error?.error?.exception || [];
const hasExceptions = exceptions.length > 0;
const logStackframes = error?.error.log?.stacktrace;
const logStackframes = error?.error?.log?.stacktrace;
const isPlaintextException = hasExceptions
? !!error.error.stack_trace && exceptions.length === 1 && !exceptions[0].stacktrace
: !!error.error.stack_trace;
? !!error?.error?.stack_trace && exceptions.length === 1 && !exceptions[0].stacktrace
: !!error?.error?.stack_trace;

switch (currentTab.key) {
case ErrorTabKey.LogStackTrace:
return <Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />;
case ErrorTabKey.ExceptionStacktrace:
return isPlaintextException ? (
<PlaintextStacktrace
message={hasExceptions ? exceptions[0].message : undefined}
type={hasExceptions ? exceptions[0].type : undefined}
stacktrace={error?.error.stack_trace}
message={hasExceptions ? exceptions[0]?.message : undefined}
type={hasExceptions ? exceptions[0]?.type : undefined}
stacktrace={error?.error?.stack_trace}
codeLanguage={codeLanguage}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render } from '@testing-library/react';
import { SampleSummary } from './sample_summary';

describe('SampleSummary (missing service / OTel incomplete data)', () => {
it('renders nothing when error is undefined', () => {
const { container } = render(<SampleSummary />);
expect(container.firstChild).toBeNull();
});

it('renders nothing when error.error is undefined', () => {
const { container } = render(<SampleSummary error={{ error: undefined as any }} />);
expect(container.firstChild).toBeNull();
});
});
Loading
Loading