[Journeys] Set traceparent for Playwright (#189800)

Sets the traceparent for Playwright, so the trace from the test runner
includes the trace events from the browser and Kibana server.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2025-02-05 18:53:34 +01:00 committed by GitHub
parent fe9023efff
commit 27893f5fcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 83 additions and 8 deletions

View file

@ -16,7 +16,7 @@ export const JOURNEY_APM_CONFIG = {
secretToken: APM_PUBLIC_TOKEN,
active: 'true',
contextPropagationOnly: 'false',
environment: process.env.CI ? 'ci' : 'development',
environment: process.env.ELASTIC_APM_ENVIRONMENT || (process.env.CI ? 'ci' : 'development'),
transactionSampleRate: '1.0',
// capture request body for both errors and request transactions
// https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#capture-body

View file

@ -12,6 +12,7 @@ import { inspect, format } from 'util';
import { setTimeout as setTimer } from 'timers/promises';
import * as Rx from 'rxjs';
import apmNode from 'elastic-apm-node';
import type { ApmBase } from '@elastic/apm-rum';
import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession, Request } from 'playwright';
import { asyncMap, asyncForEach } from '@kbn/std';
import { ToolingLog } from '@kbn/tooling-log';
@ -32,6 +33,13 @@ import { JourneyScreenshots } from './journey_screenshots';
import { getNewPageObject } from '../services/page';
import { getSynthtraceClient } from '../services/synthtrace';
type WindowWithApmContext = Window & {
elasticApm?: ApmBase;
traceIdOverrideListenerAttached?: boolean;
journeyTraceId?: string;
journeyParentId?: string;
};
export class JourneyFtrHarness {
private readonly screenshots: JourneyScreenshots;
private readonly kbnUrl: KibanaUrl;
@ -161,6 +169,53 @@ export class JourneyFtrHarness {
this.page = await this.context.newPage();
await this.interceptBrowserRequests(this.page);
const initializeApmInitListener = async () => {
await this.page?.evaluate(() => {
const win = window as WindowWithApmContext;
const attachTraceIdOverrideListener = () => {
if (win.traceIdOverrideListenerAttached) {
return;
}
win.traceIdOverrideListenerAttached = true;
win.elasticApm!.observe('transaction:start', (tx) => {
// private properties, bit of a hack
// @ts-expect-error
tx.traceId = win.journeyTraceId || tx.traceId;
// @ts-expect-error
tx.parentId = win.journeyParentId || tx.parentId;
});
};
if (win.elasticApm) {
attachTraceIdOverrideListener();
} else {
// attach trace listener as soon as elasticApm API is available
let originalValue: any;
Object.defineProperty(window, 'elasticApm', {
get() {
return originalValue;
},
set(newValue) {
originalValue = newValue;
attachTraceIdOverrideListener();
},
configurable: true,
enumerable: true,
});
}
});
};
this.page.on('framenavigated', () => {
initializeApmInitListener();
});
await initializeApmInitListener();
if (!process.env.NO_BROWSER_LOG) {
this.page.on('console', this.onConsoleEvent);
}
@ -168,7 +223,6 @@ export class JourneyFtrHarness {
await this.sendCDPCommands(this.context, this.page);
this.trackTelemetryRequests(this.page);
await this.interceptBrowserRequests(this.page);
}
private async runSynthtrace() {
@ -382,9 +436,12 @@ export class JourneyFtrHarness {
}
}
private getCurrentSpanOrTransaction() {
return this.currentSpanStack.length ? this.currentSpanStack[0] : this.currentTransaction;
}
private getCurrentTraceparent() {
return (this.currentSpanStack.length ? this.currentSpanStack[0] : this.currentTransaction)
?.traceparent;
return this.getCurrentSpanOrTransaction()?.traceparent;
}
private async getBrowserInstance() {
@ -514,6 +571,18 @@ export class JourneyFtrHarness {
for (const step of steps) {
it(step.name, async () => {
await this.withSpan(`step: ${step.name}`, 'step', async () => {
await this.page?.evaluate(
([traceId, parentId]) => {
const win = window as WindowWithApmContext;
win.journeyTraceId = traceId;
win.journeyParentId = parentId;
},
[
this.apm?.currentTraceIds['trace.id'],
this.apm?.currentTraceIds['span.id'] || this.apm?.currentTraceIds['transaction.id'],
]
);
try {
await step.fn(this.getCtx());
await this.onStepSuccess(step);

View file

@ -81,7 +81,9 @@ describe('getApmConfig', () => {
agentMock.currentTransaction = {
sampled: 'sampled',
traceId: 'traceId',
ensureParentId: () => 'parentId',
ids: {
['transaction.id']: 'transactionId',
},
} as any;
const config = getApmConfig('/some-other-path');
@ -90,7 +92,7 @@ describe('getApmConfig', () => {
expect.objectContaining({
pageLoadTraceId: 'traceId',
pageLoadSampled: 'sampled',
pageLoadSpanId: 'parentId',
pageLoadParentId: 'transactionId',
})
);
});

View file

@ -46,7 +46,8 @@ export const getApmConfig = (requestPath: string) => {
...config,
pageLoadTraceId: traceId,
pageLoadSampled: sampled,
pageLoadSpanId: backendTransaction.ensureParentId(),
pageLoadParentId:
agent.currentSpan?.ids['span.id'] || agent.currentTransaction?.ids['transaction.id'],
};
}

View file

@ -109,7 +109,8 @@ async function startEs(props: EsRunProps) {
'scripts/es',
'snapshot',
'--license=trial',
// Temporarily disabling APM
// Temporarily disabling APM because the token needs to be added to the keystore
// and cannot be set via command line
// ...(JOURNEY_APM_CONFIG.active
// ? [
// '-E',
@ -155,6 +156,8 @@ async function runFunctionalTest(props: TestRunProps) {
k.startsWith('ELASTIC_APM_') ? [[k, undefined]] : []
)
),
ELASTIC_APM_ENVIRONMENT: process.env.ELASTIC_APM_ENVIRONMENT,
ELASTIC_APM_SERVICE_NAME: process.env.ELASTIC_APM_SERVICE_NAME,
TEST_PERFORMANCE_PHASE: phase,
TEST_ES_URL: 'http://elastic:changeme@localhost:9200',
TEST_ES_DISABLE_STARTUP: 'true',