[APM][OTel] Fix span url link when transactionId missing in Span Links (#218232)

Closes #214557

### Summary

Use `traceId` to generate url link to Span as fallback when
`transactionId` is missing.

### How to test

1. Run the following synthtrace scenario: `node scripts/synthtrace
span_links.ts --live --uniqueIds --clean --logLevel=debug --scenarioOpts
pipeline=apmToOtel`
2. Go to **Service** -> **Transactions** -> in **Transaction waterfall**
click **Span Links** label -> select **Outgoing links** from downdown ->
check if **Span** link works


https://github.com/user-attachments/assets/c22fdc5e-7ba9-4817-a78b-bf5fb9a53651

---------

Co-authored-by: jennypavlova <dzheni.pavlova@elastic.co>
This commit is contained in:
Milosz Marcinkowski 2025-04-22 15:24:21 +02:00 committed by GitHub
parent 56b3e21fc3
commit 8caec69036
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 221 additions and 190 deletions

View file

@ -14,6 +14,7 @@ export const TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR = 'TRANSACTION_DETAILS_BY_T
export interface TransactionDetailsByTraceIdLocatorParams extends SerializableRecord {
rangeFrom?: string;
rangeTo?: string;
waterfallItemId?: string;
traceId: string;
}

View file

@ -43,11 +43,13 @@ export {
setIdGeneratorStrategy,
} from './src/lib/utils/generate_id';
export { appendHash, hashKeysOf } from './src/lib/utils/hash';
export type {
ESDocumentWithOperation,
SynthtraceESAction,
SynthtraceGenerator,
SynthtraceDynamicTemplate,
export {
type ESDocumentWithOperation,
type SynthtraceESAction,
type SynthtraceGenerator,
type SynthtraceDynamicTemplate,
type ApmSynthtracePipelines,
ApmSynthtracePipelineSchema,
} from './src/types';
export { log, type LogDocument, LONG_FIELD_NAME } from './src/lib/logs';
export { otelLog, type OtelLogDocument } from './src/lib/otel_logs';

View file

@ -0,0 +1,18 @@
/*
* 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".
*/
export enum ApmSynthtracePipelineSchema {
Default = 'default', // classic APM
Otel = 'otel', // OTel native through APM server
ApmToOtel = 'apmToOtel', // convert classic APM synthtrace scenario into OTel native (useful to run existing scenarios as OTel)
}
export type ApmSynthtracePipelines =
| ApmSynthtracePipelineSchema.Default
| ApmSynthtracePipelineSchema.Otel
| ApmSynthtracePipelineSchema.ApmToOtel;

View file

@ -25,3 +25,8 @@ export type SynthtraceGenerator<TFields extends Fields> = Generator<Serializable
export type SynthtraceProcessor<TFields extends Fields> = (
fields: ESDocumentWithOperation<TFields>
) => ESDocumentWithOperation<TFields>;
export {
ApmSynthtracePipelineSchema,
type ApmSynthtracePipelines,
} from './apm_synthtrace_pipelines';

View file

@ -14,10 +14,7 @@ export {
extendToolingLog,
} from './src/lib/utils/create_logger';
export {
ApmSynthtraceEsClient,
type ApmSynthtracePipelines,
} from './src/lib/apm/client/apm_synthtrace_es_client';
export { ApmSynthtraceEsClient } from './src/lib/apm/client/apm_synthtrace_es_client';
export { ApmSynthtraceKibanaClient } from './src/lib/apm/client/apm_synthtrace_kibana_client';
export { InfraSynthtraceEsClient } from './src/lib/infra/infra_synthtrace_es_client';
export { InfraSynthtraceKibanaClient } from './src/lib/infra/infra_synthtrace_kibana_client';

View file

@ -8,7 +8,7 @@
*/
import { Client, estypes } from '@elastic/elasticsearch';
import { ApmFields, ApmOtelFields } from '@kbn/apm-synthtrace-client';
import { ApmFields, ApmOtelFields, ApmSynthtracePipelines } from '@kbn/apm-synthtrace-client';
import { ValuesType } from 'utility-types';
import { SynthtraceEsClient, SynthtraceEsClientOptions } from '../../../shared/base_client';
import { Logger } from '../../../utils/create_logger';
@ -25,7 +25,6 @@ export enum ComponentTemplateName {
TracesApmRum = 'traces-apm.rum@custom',
TracesApmSampled = 'traces-apm.sampled@custom',
}
export type ApmSynthtracePipelines = 'default' | 'otelToApm' | 'apmToOtel';
export interface ApmSynthtraceEsClientOptions extends Omit<SynthtraceEsClientOptions, 'pipeline'> {
version: string;
@ -96,7 +95,7 @@ export class ApmSynthtraceEsClient extends SynthtraceEsClient<ApmFields | ApmOte
} = { includeSerialization: true }
) {
switch (pipeline) {
case 'otelToApm': {
case 'otel': {
return otelToApmPipeline(this.logger, options.includeSerialization);
}
case 'apmToOtel': {

View file

@ -7,16 +7,20 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ApmSynthtracePipelines } from '../../lib/apm/client/apm_synthtrace_es_client';
import { ApmSynthtracePipelineSchema, ApmSynthtracePipelines } from '@kbn/apm-synthtrace-client';
const validPipelines: ApmSynthtracePipelines[] = ['apmToOtel', 'otelToApm', 'default'];
const validPipelines: ApmSynthtracePipelines[] = [
ApmSynthtracePipelineSchema.ApmToOtel,
ApmSynthtracePipelineSchema.Otel,
ApmSynthtracePipelineSchema.Default,
];
const parseApmPipeline = (value: ApmSynthtracePipelines): ApmSynthtracePipelines => {
if (!value) return 'default';
if (!value) return ApmSynthtracePipelineSchema.Default;
if (validPipelines.includes(value)) {
return value;
} else {
return 'default';
return ApmSynthtracePipelineSchema.Default;
}
};

View file

@ -7,7 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { OtelInstance, ApmOtelFields } from '@kbn/apm-synthtrace-client';
import {
OtelInstance,
ApmOtelFields,
ApmSynthtracePipelineSchema,
} from '@kbn/apm-synthtrace-client';
import { apm } from '@kbn/apm-synthtrace-client/src/lib/apm';
import { Scenario } from '../cli/scenario';
import { withClient } from '../lib/utils/with_client';
@ -18,7 +22,7 @@ const ENVIRONMENT = getSynthtraceEnvironment(__filename);
const scenario: Scenario<ApmOtelFields> = async (runOptions) => {
return {
bootstrap: async ({ apmEsClient }) => {
apmEsClient.pipeline(apmEsClient.getPipeline('otelToApm'));
apmEsClient.pipeline(apmEsClient.getPipeline(ApmSynthtracePipelineSchema.Otel));
},
generate: ({ range, clients: { apmEsClient } }) => {
const transactionName = 'oteldemo.AdServiceSynth/GetAds';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ApmFields, apm } from '@kbn/apm-synthtrace-client';
import { ApmFields, ApmSynthtracePipelineSchema, apm } from '@kbn/apm-synthtrace-client';
import { random } from 'lodash';
import semver from 'semver';
import { Scenario } from '../cli/scenario';
@ -25,7 +25,7 @@ const scenario: Scenario<ApmFields> = async ({
bootstrap: async ({ apmEsClient }) => {
if (isLegacy) {
apmEsClient.pipeline(
apmEsClient.getPipeline('default', {
apmEsClient.getPipeline(ApmSynthtracePipelineSchema.Default, {
versionOverride: version,
})
);

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client';
import { ApmFields, apm, Instance, ApmSynthtracePipelineSchema } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
import { withClient } from '../lib/utils/with_client';
@ -18,7 +18,9 @@ const ENVIRONMENT = getSynthtraceEnvironment(__filename);
const scenario: Scenario<ApmFields> = async (runOptions) => {
const { logger } = runOptions;
const { numServices = 3, pipeline = 'default' } = parseApmScenarioOpts(runOptions.scenarioOpts);
const { numServices = 3, pipeline = ApmSynthtracePipelineSchema.Default } = parseApmScenarioOpts(
runOptions.scenarioOpts
);
return {
bootstrap: async ({ apmEsClient }) => {

View file

@ -12,6 +12,7 @@ import { Readable } from 'stream';
import {
apm,
ApmFields,
ApmSynthtracePipelineSchema,
generateLongId,
generateShortId,
Serializable,
@ -40,7 +41,7 @@ function getSpanLinksFromEvents(events: ApmFields[]) {
}
const scenario: Scenario<ApmFields> = async ({ logger, scenarioOpts }) => {
const { pipeline = 'default' } = parseApmScenarioOpts(scenarioOpts);
const { pipeline = ApmSynthtracePipelineSchema.Default } = parseApmScenarioOpts(scenarioOpts);
return {
bootstrap: async ({ apmEsClient }) => {
apmEsClient.pipeline(apmEsClient.getPipeline(pipeline));

View file

@ -6,6 +6,7 @@
*/
import url from 'url';
import { ApmSynthtracePipelineSchema } from '@kbn/apm-synthtrace-client';
import { synthtrace } from '../../../synthtrace';
import { adserviceEdot } from '../../fixtures/synthtrace/adservice_edot';
import { checkA11y } from '../../support/commands';
@ -33,7 +34,7 @@ describe('Service Overview', () => {
from: new Date(start).getTime(),
to: new Date(end).getTime(),
}),
'otelToApm'
ApmSynthtracePipelineSchema.Otel
);
});

View file

@ -6,6 +6,7 @@
*/
import url from 'url';
import { ApmSynthtracePipelineSchema } from '@kbn/apm-synthtrace-client';
import { synthtrace } from '../../../synthtrace';
import { sendotlp } from '../../fixtures/synthtrace/sendotlp';
import { checkA11y } from '../../support/commands';
@ -33,7 +34,7 @@ describe('Service Overview', () => {
from: new Date(start).getTime(),
to: new Date(end).getTime(),
}),
'otelToApm'
ApmSynthtracePipelineSchema.Otel
);
});

View file

@ -4,12 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ApmSynthtracePipelines } from '@kbn/apm-synthtrace';
import { ApmSynthtraceEsClient, createLogger, LogLevel } from '@kbn/apm-synthtrace';
import { createEsClientForTesting } from '@kbn/test';
// eslint-disable-next-line @kbn/imports/no_unresolvable_imports
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins';
import { Readable } from 'stream';
import type { ApmSynthtracePipelines } from '@kbn/apm-synthtrace-client';
export function setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {
const logger = createLogger(LogLevel.info);

View file

@ -4,18 +4,19 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ApmSynthtracePipelines } from '@kbn/apm-synthtrace';
import type {
Serializable,
ApmFields,
ApmOtelFields,
SynthtraceGenerator,
import {
type Serializable,
type ApmFields,
type ApmOtelFields,
type SynthtraceGenerator,
type ApmSynthtracePipelines,
ApmSynthtracePipelineSchema,
} from '@kbn/apm-synthtrace-client';
export const synthtrace = {
index: <TFields extends ApmFields | ApmOtelFields>(
events: SynthtraceGenerator<TFields> | Array<Serializable<TFields>>,
pipeline: ApmSynthtracePipelines = 'default'
pipeline: ApmSynthtracePipelines = ApmSynthtracePipelineSchema.Default
) =>
cy.task('synthtrace:index', {
events: Array.from(events).flatMap((event) => event.serialize()),

View file

@ -29,7 +29,7 @@ export function TraceLink() {
const timeRange = dataService.query.timefilter.timefilter.getTime();
const {
path: { traceId },
query: { rangeFrom = timeRange.from, rangeTo = timeRange.to },
query: { rangeFrom = timeRange.from, rangeTo = timeRange.to, waterfallItemId },
} = useApmParams('/link-to/trace/{traceId}');
const { start, end } = useTimeRange({
@ -53,6 +53,7 @@ export function TraceLink() {
transaction: data.transaction,
rangeFrom,
rangeTo,
waterfallItemId,
})
: getRedirectToTracePageUrl({ traceId, rangeFrom, rangeTo });
return <Redirect to={to} />;

View file

@ -102,7 +102,7 @@ describe('TraceLink', () => {
});
describe('transaction page', () => {
it('renders with date range params', () => {
it('renders with date range and waterfall params', () => {
const transaction = {
service: { name: 'foo' },
transaction: {
@ -125,13 +125,14 @@ describe('TraceLink', () => {
query: {
rangeFrom: 'now-24h',
rangeTo: 'now',
waterfallItemId: '789',
},
});
const component = shallow(<TraceLink />);
expect(component.prop('to')).toEqual(
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now&waterfallItemId='
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now&waterfallItemId=789'
);
});

View file

@ -88,6 +88,7 @@ const apmRoutes = {
query: t.partial({
rangeFrom: t.string,
rangeTo: t.string,
waterfallItemId: t.string,
}),
}),
]),

View file

@ -20,6 +20,8 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR } from '@kbn/deeplinks-observability/locators';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import type { SpanLinkDetails } from '../../../../common/span_links';
import { asDuration } from '../../../../common/utils/formatters';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
@ -40,6 +42,18 @@ export function SpanLinksTable({ items }: Props) {
'/mobile-services/{serviceName}/transactions/view'
);
const [idActionMenuOpen, setIdActionMenuOpen] = useState<string | undefined>();
const {
share: {
url: { locators },
},
} = useApmPluginContext();
const apmLinkToTransactionByTraceIdLocator = locators.get<{
traceId: string;
rangeFrom: string;
rangeTo: string;
waterfallItemId: string;
}>(TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR);
const columns: Array<EuiBasicTableColumn<SpanLinkDetails>> = [
{
@ -100,7 +114,7 @@ export function SpanLinksTable({ items }: Props) {
}),
sortable: true,
render: (_, { spanId, traceId, details }) => {
if (details && details.transactionId) {
if (details) {
return (
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
@ -109,10 +123,19 @@ export function SpanLinksTable({ items }: Props) {
<EuiFlexItem>
<EuiLink
data-test-subj="apmColumnsLink"
href={router.link('/link-to/transaction/{transactionId}', {
path: { transactionId: details.transactionId },
query: { waterfallItemId: spanId },
})}
href={
details.transactionId
? router.link('/link-to/transaction/{transactionId}', {
path: { transactionId: details.transactionId },
query: { waterfallItemId: spanId },
})
: apmLinkToTransactionByTraceIdLocator?.getRedirectUrl({
traceId,
rangeFrom,
rangeTo,
waterfallItemId: spanId,
})
}
>
{details.spanName}
</EuiLink>

View file

@ -98,7 +98,6 @@ export { useTimeBuckets } from './hooks/use_time_buckets';
export { createUseRulesLink } from './hooks/create_use_rules_link';
export { useSummaryTimeRange } from './hooks/use_summary_time_range';
export { getApmTraceUrl } from './utils/get_apm_trace_url';
export { buildEsQuery } from './utils/build_es_query';
export type {

View file

@ -1,16 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getApmTraceUrl } from './get_apm_trace_url';
describe('getApmTraceUrl', () => {
it('returns a trace url', () => {
expect(getApmTraceUrl({ traceId: 'foo', rangeFrom: '123', rangeTo: '456' })).toEqual(
'/link-to/trace/foo?rangeFrom=123&rangeTo=456'
);
});
});

View file

@ -1,18 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export function getApmTraceUrl({
traceId,
rangeFrom,
rangeTo,
}: {
traceId: string;
rangeFrom: string;
rangeTo: string;
}) {
return `/link-to/trace/${traceId}?` + new URLSearchParams({ rangeFrom, rangeTo }).toString();
}

View file

@ -24,9 +24,10 @@ export class TransactionDetailsByTraceIdLocatorDefinition
public readonly getLocation = async ({
rangeFrom,
rangeTo,
waterfallItemId,
traceId,
}: TransactionDetailsByTraceIdLocatorParams) => {
const params = { rangeFrom, rangeTo };
const params = { rangeFrom, rangeTo, waterfallItemId };
return {
app: 'apm',
path: `/link-to/trace/${encodeURIComponent(traceId)}?${qs.stringify(params)}`,

View file

@ -7,8 +7,9 @@
import expect from '@kbn/expect';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { Readable } from 'stream';
import type { ApmSynthtraceEsClient, ApmSynthtracePipelines } from '@kbn/apm-synthtrace';
import { type ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import moment from 'moment';
import { ApmSynthtracePipelineSchema, ApmSynthtracePipelines } from '@kbn/apm-synthtrace-client';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
import { generateSpanLinksData } from './data_generator';
@ -20,17 +21,16 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
const start = moment(baseTime).subtract(15, 'minutes');
const end = moment(baseTime);
const scenarios: ApmSynthtracePipelines[] = ['default', 'apmToOtel'];
// skips all tests that validate the `transaction.id` in `span` docs for Otel
// TODO: remove this once a solution for that has been found
const maybeIt = (pipeline: ApmSynthtracePipelines) => (pipeline === 'apmToOtel' ? it.skip : it);
const scenarios: ApmSynthtracePipelines[] = [
ApmSynthtracePipelineSchema.Default,
ApmSynthtracePipelineSchema.ApmToOtel,
];
describe('Span Links', () => {
scenarios.forEach((pipeline) => {
describe(`contains linked children - ${
pipeline === 'default' ? 'elastic APM' : 'Otel'
}`, () => {
const isDefaultPipeline = pipeline === ApmSynthtracePipelineSchema.Default;
describe(`contains linked children - ${isDefaultPipeline ? 'elastic APM' : 'Otel'}`, () => {
let ids: ReturnType<typeof generateSpanLinksData>['ids'];
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
@ -234,7 +234,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
expect(spanALinksDetails.parentsLinks.spanLinksDetails).to.eql([]);
});
maybeIt(pipeline)('returns two children on Span A', () => {
it('returns two children on Span A', () => {
expect(spanALinksDetails.childrenLinks.spanLinksDetails.length).to.eql(2);
const serviceCDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find(
(childDetails) => {
@ -312,7 +312,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
]);
});
maybeIt(pipeline)('returns consumer-multiple as child on Span B', () => {
it('returns consumer-multiple as child on Span B', () => {
expect(spanBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
expect(spanBLinksDetails.childrenLinks.spanLinksDetails).to.eql([
{
@ -321,12 +321,14 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
details: {
serviceName: 'consumer-multiple',
agentName: 'nodejs',
transactionId: ids.producerMultiple.transactionDId,
spanName: 'Span E',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
...(isDefaultPipeline && {
transactionId: ids.producerMultiple.transactionDId,
}),
},
},
]);
@ -358,46 +360,45 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
spanCLinksDetails = spanALinksDetailsResponse;
});
maybeIt(pipeline)(
'returns producer-internal-only Span A, producer-external-only Transaction B, and External link as parents of Transaction C',
() => {
expect(transactionCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(3);
expect(transactionCLinksDetails.parentsLinks.spanLinksDetails).to.eql([
{
traceId: ids.producerInternalOnly.traceId,
spanId: ids.producerInternalOnly.spanAId,
details: {
serviceName: 'producer-internal-only',
agentName: 'go',
it('returns producer-internal-only Span A, producer-external-only Transaction B, and External link as parents of Transaction C', () => {
expect(transactionCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(3);
expect(transactionCLinksDetails.parentsLinks.spanLinksDetails).to.eql([
{
traceId: ids.producerInternalOnly.traceId,
spanId: ids.producerInternalOnly.spanAId,
details: {
serviceName: 'producer-internal-only',
agentName: 'go',
spanName: 'Span A',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
...(isDefaultPipeline && {
transactionId: ids.producerInternalOnly.transactionAId,
spanName: 'Span A',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
},
}),
},
{
traceId: ids.producerExternalOnly.traceId,
spanId: ids.producerExternalOnly.transactionBId,
details: {
serviceName: 'producer-external-only',
agentName: 'java',
transactionId: ids.producerExternalOnly.transactionBId,
duration: 1000000,
spanName: 'Transaction B',
environment: 'production',
},
},
{
traceId: ids.producerExternalOnly.traceId,
spanId: ids.producerExternalOnly.transactionBId,
details: {
serviceName: 'producer-external-only',
agentName: 'java',
transactionId: ids.producerExternalOnly.transactionBId,
duration: 1000000,
spanName: 'Transaction B',
environment: 'production',
},
{
traceId: ids.producerConsumer.externalTraceId,
spanId: ids.producerExternalOnly.spanBId,
},
]);
}
);
},
{
traceId: ids.producerConsumer.externalTraceId,
spanId: ids.producerExternalOnly.spanBId,
},
]);
});
maybeIt(pipeline)('returns consumer-multiple Span E as child of Transaction C', () => {
it('returns consumer-multiple Span E as child of Transaction C', () => {
expect(transactionCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
expect(transactionCLinksDetails.childrenLinks.spanLinksDetails).to.eql([
{
@ -406,12 +407,14 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
details: {
serviceName: 'consumer-multiple',
agentName: 'nodejs',
transactionId: ids.producerMultiple.transactionDId,
spanName: 'Span E',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
...(isDefaultPipeline && {
transactionId: ids.producerMultiple.transactionDId,
}),
},
},
]);
@ -421,7 +424,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
expect(spanCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(0);
});
maybeIt(pipeline)('returns consumer-multiple as Child on producer-consumer', () => {
it('returns consumer-multiple as Child on producer-consumer', () => {
expect(spanCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
expect(spanCLinksDetails.childrenLinks.spanLinksDetails).to.eql([
{
@ -465,82 +468,82 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
spanELinksDetails = spanALinksDetailsResponse;
});
maybeIt(pipeline)(
'returns producer-internal-only Span A and producer-consumer Span C as parents of Transaction D',
() => {
expect(transactionDLinksDetails.parentsLinks.spanLinksDetails.length).to.be(2);
expect(transactionDLinksDetails.parentsLinks.spanLinksDetails).to.eql([
{
traceId: ids.producerInternalOnly.traceId,
spanId: ids.producerInternalOnly.spanAId,
details: {
serviceName: 'producer-internal-only',
agentName: 'go',
it('returns producer-internal-only Span A and producer-consumer Span C as parents of Transaction D', () => {
expect(transactionDLinksDetails.parentsLinks.spanLinksDetails.length).to.be(2);
expect(transactionDLinksDetails.parentsLinks.spanLinksDetails).to.eql([
{
traceId: ids.producerInternalOnly.traceId,
spanId: ids.producerInternalOnly.spanAId,
details: {
serviceName: 'producer-internal-only',
agentName: 'go',
spanName: 'Span A',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
...(isDefaultPipeline && {
transactionId: ids.producerInternalOnly.transactionAId,
spanName: 'Span A',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
},
}),
},
{
traceId: ids.producerConsumer.traceId,
spanId: ids.producerConsumer.spanCId,
details: {
serviceName: 'producer-consumer',
agentName: 'ruby',
},
{
traceId: ids.producerConsumer.traceId,
spanId: ids.producerConsumer.spanCId,
details: {
serviceName: 'producer-consumer',
agentName: 'ruby',
spanName: 'Span C',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
...(isDefaultPipeline && {
transactionId: ids.producerConsumer.transactionCId,
spanName: 'Span C',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
},
}),
},
]);
}
);
},
]);
});
it('returns no children on Transaction D', () => {
expect(transactionDLinksDetails.childrenLinks.spanLinksDetails.length).to.be(0);
});
maybeIt(pipeline)(
'returns producer-external-only Span B and producer-consumer Transaction C as parents of Span E',
() => {
expect(spanELinksDetails.parentsLinks.spanLinksDetails.length).to.be(2);
it('returns producer-external-only Span B and producer-consumer Transaction C as parents of Span E', () => {
expect(spanELinksDetails.parentsLinks.spanLinksDetails.length).to.be(2);
expect(spanELinksDetails.parentsLinks.spanLinksDetails).to.eql([
{
traceId: ids.producerExternalOnly.traceId,
spanId: ids.producerExternalOnly.spanBId,
details: {
serviceName: 'producer-external-only',
agentName: 'java',
expect(spanELinksDetails.parentsLinks.spanLinksDetails).to.eql([
{
traceId: ids.producerExternalOnly.traceId,
spanId: ids.producerExternalOnly.spanBId,
details: {
serviceName: 'producer-external-only',
agentName: 'java',
spanName: 'Span B',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
...(isDefaultPipeline && {
transactionId: ids.producerExternalOnly.transactionBId,
spanName: 'Span B',
duration: 100000,
spanSubtype: 'http',
spanType: 'external',
environment: 'production',
},
}),
},
{
traceId: ids.producerConsumer.traceId,
spanId: ids.producerConsumer.transactionCId,
details: {
serviceName: 'producer-consumer',
agentName: 'ruby',
transactionId: ids.producerConsumer.transactionCId,
spanName: 'Transaction C',
duration: 1000000,
environment: 'production',
},
},
{
traceId: ids.producerConsumer.traceId,
spanId: ids.producerConsumer.transactionCId,
details: {
serviceName: 'producer-consumer',
agentName: 'ruby',
transactionId: ids.producerConsumer.transactionCId,
spanName: 'Transaction C',
duration: 1000000,
environment: 'production',
},
]);
}
);
},
]);
});
it('returns no children on Span E', () => {
expect(spanELinksDetails.childrenLinks.spanLinksDetails.length).to.be(0);