Repository: elastic/kibana
Pull request #217885: [APM][OTel] EDOT error summary fix
Author: login: jennypavlova
Labels: name: release_note:fix
name: v9.0.0
name: ci:cloud-redeploy
description: Always create a new Cloud deployment
name: ci:build-serverless-image
name: ci:project-deploy-observability
description: Create an Observability project
name: Team:obs-ux-infra_services - DEPRECATED
description: DEPRECATED - Use Team:obs-presentation.
name: backport:version
description: Backport to applied version labels
name: v9.1.0
name: v8.19.0
name: v9.0.1
Created: 2025-04-10T16:35:58Z | Merged: 2025-04-15T19:44:11Z

## Summary

This PR fixes the issue with the error summary missing items using edot. It includes e2e tests with synthtrace for both edot and otel services.

TODO 

- [x] Test with serverless (waiting for the PR to be deployed)
Tested on serverless works as expected: 

<img width="2560" alt="image" src="https://github.com/user-attachments/assets/8dd7962e-7d66-482d-97fb-0b08882bd04f" />


<!--ONMERGE {"backportTargets":["8.x","9.0"]} ONMERGE-->


Diff:
diff --git a/x-pack/platform/packages/shared/kbn-apm-types/src/es_schemas/raw/fields/server.ts b/x-pack/platform/packages/shared/kbn-apm-types/src/es_schemas/raw/fields/server.ts
index 49e3520f19fce..c170426e4f736 100644
--- a/x-pack/platform/packages/shared/kbn-apm-types/src/es_schemas/raw/fields/server.ts
+++ b/x-pack/platform/packages/shared/kbn-apm-types/src/es_schemas/raw/fields/server.ts
@@ -7,5 +7,5 @@
 
 export interface Server {
   address?: string;
-  port?: string;
+  port?: number;
 }
diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_edot_service_overview_and_transactions.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_edot_service_overview_and_transactions.cy.ts
index b8862d53884df..9def718fb627e 100644
--- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_edot_service_overview_and_transactions.cy.ts
+++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_edot_service_overview_and_transactions.cy.ts
@@ -124,12 +124,27 @@ describe('Service Overview', () => {
     beforeEach(() => {
       cy.loginAsViewerUser();
       cy.visitKibana(baseUrl);
+      cy.contains('adservice-edot-synth');
+      cy.contains('a', 'View errors').click();
     });
 
     it('navigates to the errors page', () => {
-      cy.contains('adservice-edot-synth');
-      cy.contains('a', 'View errors').click();
       cy.url().should('include', '/adservice-edot-synth/errors');
     });
+
+    it('navigates to error detail page and shows error summary', () => {
+      cy.contains('a', '[ResponseError] index_not_found_exception').click();
+      cy.contains('div', '[ResponseError] index_not_found_exception');
+
+      cy.getByTestSubj('apmHttpInfoRequestMethod').should('exist');
+      cy.getByTestSubj('apmHttpInfoRequestMethod').contains('GET');
+      cy.getByTestSubj('apmHttpInfoUrl').should('exist');
+      cy.getByTestSubj('apmHttpInfoUrl').contains(
+        'https://otel-demo-blue-adservice-edot-synth:8080/some/path'
+      );
+      cy.getByTestSubj('apmHttpInfoRequestMethod').should('exist');
+      cy.getByTestSubj('apmHttpStatusBadge').should('exist');
+      cy.getByTestSubj('apmHttpStatusBadge').contains('OK');
+    });
   });
 });
diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_service_overview_and_transactions.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_service_overview_and_transactions.cy.ts
index 7f47372fe425d..dde47a70d77f6 100644
--- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_service_overview_and_transactions.cy.ts
+++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/service_overview/otel_service_overview_and_transactions.cy.ts
@@ -151,9 +151,17 @@ describe('Service Overview', () => {
       cy.url().should('include', '/sendotlp-otel-native-synth/errors');
     });
 
-    it('navigates to error detail page', () => {
-      cy.contains('a', '*errors.errorString').click();
+    it('navigates to error detail page and checks error summary', () => {
+      cy.contains('a', 'boom').click();
       cy.contains('div', 'boom');
+
+      cy.getByTestSubj('apmHttpInfoRequestMethod').should('exist');
+      cy.getByTestSubj('apmHttpInfoRequestMethod').contains('GET');
+      cy.getByTestSubj('apmHttpInfoUrl').should('exist');
+      cy.getByTestSubj('apmHttpInfoUrl').contains('https://elastic.co/');
+      cy.getByTestSubj('apmHttpInfoRequestMethod').should('exist');
+      cy.getByTestSubj('apmHttpStatusBadge').should('exist');
+      cy.getByTestSubj('apmHttpStatusBadge').contains('OK');
     });
   });
 });
diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/adservice_edot.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/adservice_edot.ts
index 18c24912b7739..784c82f7b680f 100644
--- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/adservice_edot.ts
+++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/fixtures/synthtrace/adservice_edot.ts
@@ -36,6 +36,15 @@ export function adserviceEdot({ from, to }: { from: number; to: number }) {
           'attributes.url.scheme': 'https',
         })
         .timestamp(timestamp)
+        .failure()
+        .errors(
+          edotInstance
+            .error({
+              message: '[ResponseError] index_not_found_exception',
+              type: 'ResponseError',
+            })
+            .timestamp(timestamp + 50)
+        )
         .duration(551)
         .success(),
     ]);
diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
index 4d147e78237cb..462f33b83a4a3 100644
--- a/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
+++ b/x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
@@ -56,6 +56,7 @@ import { ErrorTabKey, getTabs } from './error_tabs';
 import { ErrorUiActionsContextMenu } from './error_ui_actions_context_menu';
 import { SampleSummary } from './sample_summary';
 import { ErrorSampleContextualInsight } from './error_sample_contextual_insight';
+import { buildUrl } from '../../../../utils/build_url';
 
 const TransactionLinkName = styled.div`
   margin-left: ${({ theme }) => theme.euiTheme.size.s};
@@ -154,11 +155,22 @@ export function ErrorSampleDetails({
 
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
+  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;
+  const errorOrTransactionUserAgent = error?.user_agent
+    ? error.user_agent
+    : transaction?.user_agent;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  // To get the error data needed for the summary we use the transaction fallback in case
+  // the error data is not available.
+  // In case of OTel the error data is not available in the error response and we need to use
+  // the associated root span data (which is called "transaction" here because of the APM data model).
+  const errorUrl = urlFromError || urlFromTransaction || buildUrl(errorOrTransactionUrl);
+  const method = errorOrTransactionHttp?.http?.request?.method;
+  const status = errorOrTransactionHttp?.http?.response?.status_code;
+  const userAgent = errorOrTransactionUserAgent;
   const environment = error.service.environment;
   const serviceVersion = error.service.version;
   const isUnhandled = error.error.exception?.[0]?.handled === false;
@@ -205,7 +217,7 @@ export function ErrorSampleDetails({
                 <EuiFlexItem>
                   <EuiIcon type="apmTrace" />
                 </EuiFlexItem>
-                <EuiFlexItem style={{ whiteSpace: 'nowrap' }}>
+                <EuiFlexItem css={{ whiteSpace: 'nowrap' }}>
                   {i18n.translate('xpack.apm.errorSampleDetails.viewOccurrencesInTraceExplorer', {
                     defaultMessage: 'Explore traces with this error',
                   })}
@@ -223,7 +235,7 @@ export function ErrorSampleDetails({
               <EuiFlexItem>
                 <EuiIcon type="discoverApp" />
               </EuiFlexItem>
-              <EuiFlexItem style={{ whiteSpace: 'nowrap' }}>
+              <EuiFlexItem css={{ whiteSpace: 'nowrap' }}>
                 {i18n.translate(
                   'xpack.apm.errorSampleDetails.viewOccurrencesInDiscoverButtonLabel',
                   {
@@ -247,7 +259,7 @@ export function ErrorSampleDetails({
         <Summary
           items={[
             <TimestampTooltip time={errorData ? error.timestamp.us / 1000 : 0} />,
-            errorUrl && method ? (
+            errorUrl ? (
               <HttpInfoSummaryItem url={errorUrl} method={method} status={status} />
             ) : null,
             userAgent?.name ? <UserAgentSummaryItem {...userAgent} /> : null,
diff --git a/x-pack/solutions/observability/plugins/apm/public/utils/build_url.test.ts b/x-pack/solutions/observability/plugins/apm/public/utils/build_url.test.ts
index 603fb536418fc..5b7e8ae27a176 100644
--- a/x-pack/solutions/observability/plugins/apm/public/utils/build_url.test.ts
+++ b/x-pack/solutions/observability/plugins/apm/public/utils/build_url.test.ts
@@ -5,8 +5,7 @@
  * 2.0.
  */
 
-import { buildUrl } from './build_url';
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
+import { type ItemType, buildUrl } from './build_url';
 
 describe('buildUrl', () => {
   it('should return a full URL when all fields are provided', () => {
@@ -20,7 +19,7 @@ describe('buildUrl', () => {
         port: 443,
       },
     };
-    const result = buildUrl(item as unknown as Transaction);
+    const result = buildUrl(item);
     expect(result).toBe('ftp://example.com:443/some/path');
   });
 
@@ -34,7 +33,7 @@ describe('buildUrl', () => {
         address: 'example.org',
       },
     };
-    const result = buildUrl(item as Transaction);
+    const result = buildUrl(item);
     expect(result).toBe('http://example.org/another/path');
   });
 
@@ -48,7 +47,7 @@ describe('buildUrl', () => {
         port: 8443,
       },
     };
-    const result = buildUrl(item as unknown as Transaction);
+    const result = buildUrl(item);
     expect(result).toBe('https://example.net:8443/');
   });
 
@@ -62,7 +61,7 @@ describe('buildUrl', () => {
         port: 8080,
       },
     };
-    const result = buildUrl(item as unknown as Transaction);
+    const result = buildUrl(item);
     expect(result).toBeUndefined();
   });
 
@@ -76,7 +75,7 @@ describe('buildUrl', () => {
         port: 8080,
       },
     };
-    const result = buildUrl(item as unknown as Transaction);
+    const result = buildUrl(item);
     expect(result).toBeUndefined();
   });
 
@@ -90,17 +89,17 @@ describe('buildUrl', () => {
       },
       server: {
         address: 'example.com',
-        port: 'invalid-port',
+        port: 'invalid', // Invalid port
       },
     };
 
-    const result = buildUrl(item as unknown as Transaction);
+    const result = buildUrl(item as unknown as ItemType);
 
     expect(result).toBeUndefined();
     expect(consoleErrorSpy).toHaveBeenCalledWith(
       'Failed to build URL',
       expect.objectContaining({
-        message: 'Invalid base URL: https://example.com:invalid-port',
+        message: 'Invalid base URL: https://example.com:invalid',
       })
     );
 
@@ -109,7 +108,7 @@ describe('buildUrl', () => {
 
   it('should handle an empty object gracefully', () => {
     const item = {};
-    const result = buildUrl(item as Transaction);
+    const result = buildUrl(item);
     expect(result).toBeUndefined();
   });
 });
diff --git a/x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts b/x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
index 30f74fb58ac27..e5a16508a0d23 100644
--- a/x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
+++ b/x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
@@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+export interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: number;
+  };
+}
 
-export const buildUrl = (item: Transaction | Span | APMError) => {
+export const buildUrl = (item?: ItemType) => {
   // URL fields from Otel
   const urlScheme = item?.url?.scheme;
   const urlPath = item?.url?.path;
diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/transactions/get_transaction/index.ts b/x-pack/solutions/observability/plugins/apm/server/routes/transactions/get_transaction/index.ts
index 323e48118b080..370bd37947c50 100644
--- a/x-pack/solutions/observability/plugins/apm/server/routes/transactions/get_transaction/index.ts
+++ b/x-pack/solutions/observability/plugins/apm/server/routes/transactions/get_transaction/index.ts
@@ -126,6 +126,10 @@ export async function getTransaction({
 
   return {
     ...event,
+    server: {
+      ...event.server,
+      port: event.server?.port ? Number(event.server?.port) : undefined,
+    },
     transaction: {
       ...event.transaction,
       marks: source?.transaction?.marks,


Reviews:
user: login: gregkalapos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:11:29Z
user: login: jennypavlova
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T11:59:06Z
user: login: rmyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T09:02:50Z
user: login: rmyz
body: Code LGTM, added a small nit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T09:03:11Z
user: login: crespocarlos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T09:09:17Z
user: login: jennypavlova
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:22:03Z
user: login: rmyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:10:42Z
user: login: rmyz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:26:04Z
user: login: jennypavlova
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T09:43:02Z
user: login: crespocarlos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:49:00Z
user: login: jennypavlova
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T10:38:00Z
user: login: jennypavlova
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T11:58:42Z
user: login: rmyz
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:00:11Z
user: login: crespocarlos
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T15:57:12Z

Review comments:
diff_hunk: @@ -155,10 +156,11 @@ export function ErrorSampleDetails({
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  const errorUrl =
+    (error.error.page?.url || error.url?.full) ?? buildUrl(error?.url ? error : transaction);
+  const method = error.http?.request?.method ?? transaction?.http?.request?.method;
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: gregkalapos
body: This is just a super nit thing, and maybe not even relevant, nevertheless:

Since we want to make our product OTel native and `transaction` is not an OTel concept and we have countless issues by having it, this caught my eye. I know this is already existing code, and we need backwards compatibility, where transaction is a real thing.

Nevertheless: `transaction` here is just a variable name, that point to the root span, right? Any thought on maybe going with `rootSpan` or something like that?

This is probably all good, it just worries me a bit, that transaction keeps spreading in the code.

This is really a nit and unfortunately I don't really have a suggestion here - this is more like an fyi for consideration.
created_at: 2025-04-11T06:11:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -155,10 +156,11 @@ export function ErrorSampleDetails({
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  const errorUrl =
+    (error.error.page?.url || error.url?.full) ?? buildUrl(error?.url ? error : transaction);
+  const method = error.http?.request?.method ?? transaction?.http?.request?.method;
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: jennypavlova
body: Hey @gregkalapos, 

> Nevertheless: transaction here is just a variable name, that point to the root span, right? Any thought on maybe going with rootSpan or something like that?

I checked the logic to get the "transaction" data from the ` error`  and we use `transaction id` OR `span id` ([ref](https://github.com/jennypavlova/kibana/blob/679abcab44790e79b2dd046aee25630cc617a1bb/x-pack/solutions/observability/plugins/apm/server/routes/errors/get_error_groups/get_error_sample_details.ts#L148-L161)) so some of those fallbacks (also the existing ones before my change) were used for the APM use-cases (in case some of the information is not present in the error we were checking the associated transaction)

> Since we want to make our product OTel native and transaction is not an OTel concept and we have countless issues by having it, this caught my eye. I know this is already existing code, and we need backwards compatibility, where transaction is a real thing.

I totally agree that we should go with the OTel native approach but also the whole "error" concept is different when we compare APM and OTel. As we still use this one for APM transactions or OTel spans I would keep the `transaction` name for now (otherwise I nee to change many different places that are not part of the errors logic I am fixing here). As this might create confusion for OTel I will add a comment above to explain and will be happy to migrate to OTel schema everywhere (which would require more changes but would make it much easier to work with when we have OTel data) but anyway thanks for the comment :) 
created_at: 2025-04-11T11:59:06Z
author_association: MEMBER
diff_hunk: @@ -155,10 +156,18 @@ export function ErrorSampleDetails({
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  // To get the error data needed for the summary we use the transaction fallback in case
+  // the error data is not available.
+  // In case of OTel the error data is not available in the error response and we need to use
+  // the associated root span data (which is called "transaction" here because of the APM data model).
+  const errorUrl =
+    error.error.page?.url ||
+    error.url?.full ||
+    transaction?.url?.full ||
+    buildUrl(error?.url ? error : transaction);
+  const method = error.http?.request?.method ?? transaction?.http?.request?.method;
+  const status = error.http?.response?.status_code ?? transaction?.http?.response?.status_code;
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: rmyz
body: Can we create a variable to know if we are actually using the `error` or the `transaction` object?
I think it will be hard to debug as it is right now, since we don't know which value are we using with that many `||` and `??`.
Something like `const errorData = error.X ? error : transaction;` and use it then `const method = objectKind.http?.request?.method...`. The naming can be improved but is just an example, wdyt?

created_at: 2025-04-14T09:02:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: string;
path: x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
user: login: crespocarlos
body: Should `port` be a string? 

https://opentelemetry.io/docs/specs/semconv/attributes-registry/server/#server-port
https://www.elastic.co/guide/en/apm/server/current/exported-fields-ecs.html
created_at: 2025-04-14T09:03:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -223,7 +232,7 @@ export function ErrorSampleDetails({
               <EuiFlexItem>
                 <EuiIcon type="discoverApp" />
               </EuiFlexItem>
-              <EuiFlexItem style={{ whiteSpace: 'nowrap' }}>
+              <EuiFlexItem css={{ whiteSpace: 'nowrap' }}>
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: crespocarlos
body: nice!
created_at: 2025-04-14T09:04:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: string;
path: x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
user: login: jennypavlova
body: Do you mean just `string` and not optional? It is string now 🤔 
created_at: 2025-04-14T09:23:12Z
author_association: MEMBER
diff_hunk: @@ -155,10 +156,18 @@ export function ErrorSampleDetails({
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  // To get the error data needed for the summary we use the transaction fallback in case
+  // the error data is not available.
+  // In case of OTel the error data is not available in the error response and we need to use
+  // the associated root span data (which is called "transaction" here because of the APM data model).
+  const errorUrl =
+    error.error.page?.url ||
+    error.url?.full ||
+    transaction?.url?.full ||
+    buildUrl(error?.url ? error : transaction);
+  const method = error.http?.request?.method ?? transaction?.http?.request?.method;
+  const status = error.http?.response?.status_code ?? transaction?.http?.response?.status_code;
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: jennypavlova
body: Thanks for the comment, I understand it looks a bit confusing. I tried to extract some parts to some new variables. You can take a look at the latest changes and let me know if you think that I should change them :) 
created_at: 2025-04-14T15:21:59Z
author_association: MEMBER
diff_hunk: @@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: string;
path: x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
user: login: rmyz
body: I guess the confusion is because the Opentelemetry port is considered an Integer, while we treat it as a string
created_at: 2025-04-15T08:10:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -155,10 +156,18 @@ export function ErrorSampleDetails({
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  // To get the error data needed for the summary we use the transaction fallback in case
+  // the error data is not available.
+  // In case of OTel the error data is not available in the error response and we need to use
+  // the associated root span data (which is called "transaction" here because of the APM data model).
+  const errorUrl =
+    error.error.page?.url ||
+    error.url?.full ||
+    transaction?.url?.full ||
+    buildUrl(error?.url ? error : transaction);
+  const method = error.http?.request?.method ?? transaction?.http?.request?.method;
+  const status = error.http?.response?.status_code ?? transaction?.http?.response?.status_code;
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: rmyz
body: Thanks for applying the changes @jennypavlova !
I was thinking about checking where the data is once and then using that variable, for example:
`const source = error.http ? error : transaction`.
Then we can use `const userAgent = source?.user_agent`.
The objective would be to remove redundant checks, as we will have all the information in one place, whether it's error or transaction, does it make sense?
created_at: 2025-04-15T08:26:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -155,10 +156,18 @@ export function ErrorSampleDetails({
   const tabs = getTabs(error);
   const currentTab = getCurrentTab(tabs, detailTab) as ErrorTab;
 
-  const errorUrl = error.error.page?.url || error.url?.full;
-  const method = error.http?.request?.method;
-  const status = error.http?.response?.status_code;
-  const userAgent = error?.user_agent;
+  // To get the error data needed for the summary we use the transaction fallback in case
+  // the error data is not available.
+  // In case of OTel the error data is not available in the error response and we need to use
+  // the associated root span data (which is called "transaction" here because of the APM data model).
+  const errorUrl =
+    error.error.page?.url ||
+    error.url?.full ||
+    transaction?.url?.full ||
+    buildUrl(error?.url ? error : transaction);
+  const method = error.http?.request?.method ?? transaction?.http?.request?.method;
+  const status = error.http?.response?.status_code ?? transaction?.http?.response?.status_code;
path: x-pack/solutions/observability/plugins/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx
user: login: jennypavlova
body: Hey @rmyz, I tried to do something similar at the beginning but there is no guarantee that if the field X is present in an error all the other fields will be as well (I couldn't find such field - I know in theory this should not happen but for example the user agent fallback was added even before this otel changes here. 
```
const source = error.http ? error : transaction.
Then we can use const userAgent = source?.user_agent.
```
So the problem I see here is that we could have the `error.http` but the user_agent to come from a transaction. I kept the other check to be safe and to make sure we always have a fallback if the error values are not there 
created_at: 2025-04-15T09:43:02Z
author_association: MEMBER
diff_hunk: @@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: string;
path: x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
user: login: crespocarlos
body: it's an integer on both ECS and semconv
created_at: 2025-04-15T09:48:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: string;
path: x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
user: login: jennypavlova
body: Hmm true, same in ECS, I guess I got it mixed with a different property... I will set it to be a number everywhere, thank you both!
created_at: 2025-04-15T10:38:00Z
author_association: MEMBER
diff_hunk: @@ -4,11 +4,18 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-import type { Transaction } from '../../typings/es_schemas/ui/transaction';
-import type { Span } from '../../typings/es_schemas/ui/span';
-import type { APMError } from '../../typings/es_schemas/ui/apm_error';
+interface ItemType {
+  url?: {
+    scheme?: string;
+    path?: string;
+  };
+  server?: {
+    address?: string;
+    port?: string;
path: x-pack/solutions/observability/plugins/apm/public/utils/build_url.ts
user: login: jennypavlova
body: I pushed some other changes to make sure we convert it to number where needed and set the number type everywhere 
created_at: 2025-04-15T11:58:42Z
author_association: MEMBER

PR comments:
user: login: elasticmachine
created_at: 2025-04-11T14:29:31Z
body: Pinging @elastic/obs-ux-infra_services-team (Team:obs-ux-infra_services)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-11T14:30:16Z
body: 
### :robot: GitHub comments

<details><summary>Expand to view the GitHub comments</summary>
<p>

Just comment with:
- `/oblt-deploy` : Deploy a Kibana instance using the Observability test environments.
- `run` `docs-build` : Re-trigger the docs validation. (use unformatted text in the comment!)
</p>
</details>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-11T16:20:13Z
body: 
### :robot: GitHub comments

<details><summary>Expand to view the GitHub comments</summary>
<p>

Just comment with:
- `/oblt-deploy` : Deploy a Kibana instance using the Observability test environments.
- `run` `docs-build` : Re-trigger the docs validation. (use unformatted text in the comment!)
</p>
</details>

author_association: CONTRIBUTOR
user: login: jennypavlova
created_at: 2025-04-11T16:21:54Z
body: /oblt-deploy
author_association: MEMBER
user: login: jennypavlova
created_at: 2025-04-15T17:58:22Z
body: Check serverless with latest changes (EDOT and APM cases) ✅ 
![image](https://github.com/user-attachments/assets/a62bbd6a-eded-4165-b5d4-23d704d27752)
![image](https://github.com/user-attachments/assets/8d37a906-8fa2-4f3d-a8cd-4f29c8567c15)

author_association: MEMBER
user: login: elasticmachine
created_at: 2025-04-15T19:00:43Z
body: ## :green_heart: Build Succeeded
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/293401)
* Commit: 7e7474fc7593e9bb702b781bacdaf175da95ec65
* [Cloud Deployment](https://kibana-pr-217885.kb.us-west2.gcp.elastic-cloud.com)
* Kibana Serverless Image: `docker.elastic.co/kibana-ci/kibana-serverless:pr-217885-7e7474fc7593`

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
#### Async chunks
> Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app


| id | [before](https://github.com/elastic/kibana/commit/a4a11bb46f63ad78399f152257a883d1a35f4ce9) | [after](https://github.com/elastic/kibana/commit/7e7474fc7593e9bb702b781bacdaf175da95ec65) | diff |
| --- | --- | --- | --- |
| `apm` | 2.6MB | 2.6MB | +437.0B |



### History
* :broken_heart: [Build #293335](https://buildkite.com/elastic/kibana-pull-request/builds/293335) failed 1bca9c2a8a9059460d8568e03956944e80df1df4
* :broken_heart: [Build #293266](https://buildkite.com/elastic/kibana-pull-request/builds/293266) failed 5b3b636c70fd5a8b8217261702a02736bfadd28c
* :broken_heart: [Build #293162](https://buildkite.com/elastic/kibana-pull-request/builds/293162) failed 99b6c5b78f5598cee3c1778993f8aaccfde471a8
* :broken_heart: [Build #293128](https://buildkite.com/elastic/kibana-pull-request/builds/293128) failed caaf16f2a90a98e1c6b01d40048965bd0b5a88f8

cc @jennypavlova

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293401","number":293401,"commit":"7e7474fc7593e9bb702b781bacdaf175da95ec65"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293335","number":293335,"commit":"1bca9c2a8a9059460d8568e03956944e80df1df4"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293266","number":293266,"commit":"5b3b636c70fd5a8b8217261702a02736bfadd28c"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293162","number":293162,"commit":"99b6c5b78f5598cee3c1778993f8aaccfde471a8"},{"buildStatus":{"state":"failed","success":false,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293128","number":293128,"commit":"caaf16f2a90a98e1c6b01d40048965bd0b5a88f8"}],"number":293401}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-15T19:44:29Z
body: Starting backport for target branches: 8.x, 9.0

https://github.com/elastic/kibana/actions/runs/14478043479
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-15T19:50:41Z
body: ## 💔 All backports failed

| Status | Branch | Result |
|:------:|:------:|:------|
|❌|8.x|**Backport failed because of merge conflicts**<br><br>You might need to backport the following PRs to 8.x:<br> - [[ES\|QL] fix editor menus on safari (#218167)](https://github.com/elastic/kibana/pull/218167)<br> - [[Custom threshold rule] Allow group for ip type fields !! (#216062)](https://github.com/elastic/kibana/pull/216062)<br> - [[Security Solution][Alert flyout] Edit highlighted fields in overview tab (#216740)](https://github.com/elastic/kibana/pull/216740)<br> - [[Attack Discovery][Scheduling] Fix the attack discovery alert type (#218025)](https://github.com/elastic/kibana/pull/218025)<br> - [[ES\|QL] remove worker (#218006)](https://github.com/elastic/kibana/pull/218006)<br> - [[Solution Side Nav] Remove PanelContentProvider & support optional title in nav node (#218156)](https://github.com/elastic/kibana/pull/218156)<br> - [[Synthetics] Sync global parameters is called in the endpoints to add, edit or delete global params (#216197)](https://github.com/elastic/kibana/pull/216197)<br> - [🌊 Streams: Use better default field (#217478)](https://github.com/elastic/kibana/pull/217478)<br> - [[Security Solution][Detection Engine] Split search request building from search (#216887)](https://github.com/elastic/kibana/pull/216887)<br> - [fix: [ML] Data Frame Analytics: Analytics selection flyout missing title from announcement (#217666)](https://github.com/elastic/kibana/pull/217666)|
|❌|9.0|**Backport failed because of merge conflicts**<br><br>You might need to backport the following PRs to 9.0:<br> - [[Synthetics] Sync global parameters is called in the endpoints to add, edit or delete global params (#216197)](https://github.com/elastic/kibana/pull/216197)<br> - [Fix/lens serialize (#218082)](https://github.com/elastic/kibana/pull/218082)<br> - [Auto increase fields limit of the alert indices (#216719)](https://github.com/elastic/kibana/pull/216719)<br> - [[Console] Fix output for empty response body (#218104)](https://github.com/elastic/kibana/pull/218104)|

### Manual backport
To create the backport manually run:
```
node scripts/backport --pr 217885
```

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: jennypavlova
created_at: 2025-04-16T08:43:03Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/218390">](https://github.com/elastic/kibana/pull/218390)|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/218391">](https://github.com/elastic/kibana/pull/218391)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: MEMBER