Skip to content

Commit

Permalink
feat(tracing) Adding groups to trace via pw-api (#33081)
Browse files Browse the repository at this point in the history
Signed-off-by: René <snooz@posteo.de>
Signed-off-by: René <41592183+Snooz82@users.noreply.github.com>
Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
  • Loading branch information
Snooz82 and dgozman authored Nov 5, 2024
1 parent da4614e commit fa10bcd
Show file tree
Hide file tree
Showing 13 changed files with 356 additions and 11 deletions.
50 changes: 50 additions & 0 deletions docs/src/api/class-tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,56 @@ given name prefix inside the [`option: BrowserType.launch.tracesDir`] directory
To specify the final trace zip file name, you need to pass `path` option to
[`method: Tracing.stopChunk`] instead.

## async method: Tracing.group
* since: v1.49

Creates a new inline group within the trace, assigning any subsequent calls to this group until [method: Tracing.groupEnd] is invoked.

Groups can be nested and are similar to `test.step` in trace.
However, groups are only visualized in the trace viewer and, unlike test.step, have no effect on the test reports.

:::note Groups should not be used with Playwright Test!

This API is intended for Playwright API users that can not use `test.step`.
:::

**Usage**

```js
await context.tracing.start({ screenshots: true, snapshots: true });
await context.tracing.group('Open Playwright.dev');
// All actions between group and groupEnd will be shown in the trace viewer as a group.
const page = await context.newPage();
await page.goto('https://playwright.dev/');
await context.tracing.groupEnd();
await context.tracing.group('Open API Docs of Tracing');
await page.getByRole('link', { name: 'API' }).click();
await page.getByRole('link', { name: 'Tracing' }).click();
await context.tracing.groupEnd();
// This Trace will have two groups: 'Open Playwright.dev' and 'Open API Docs of Tracing'.
```

### param: Tracing.group.name
* since: v1.49
- `name` <[string]>

Group name shown in the actions tree in trace viewer.

### option: Tracing.group.location
* since: v1.49
- `location` ?<[Object]>
- `file` <[string]> Source file path to be shown in the trace viewer source tab.
- `line` ?<[int]> Line number in the source file.
- `column` ?<[int]> Column number in the source file

Specifies a custom location for the group start to be shown in source tab in trace viewer.
By default, location of the tracing.group() call is shown.

## async method: Tracing.groupEnd
* since: v1.49

Closes the currently open inline group in the trace.

## async method: Tracing.stop
* since: v1.12

Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/client/channelOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
return channel;
}

async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false): Promise<R> {
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
const logger = this._logger;
const apiZone = zones.zoneData<ApiZone>('apiZone');
if (apiZone)
Expand All @@ -178,7 +178,8 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
let apiName: string | undefined = stackTrace.apiName;
const frames: channels.StackFrame[] = stackTrace.frames;

isInternal = isInternal || this._isInternalType;
if (isInternal === undefined)
isInternal = this._isInternalType;
if (isInternal)
apiName = undefined;

Expand Down
12 changes: 12 additions & 0 deletions packages/playwright-core/src/client/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
await this._startCollectingStacks(traceName);
}

async group(name: string, options: { location?: { file: string, line?: number, column?: number } } = {}) {
await this._wrapApiCall(async () => {
await this._channel.tracingGroup({ name, location: options.location });
}, false);
}

async groupEnd() {
await this._wrapApiCall(async () => {
await this._channel.tracingGroupEnd();
}, false);
}

private async _startCollectingStacks(traceName: string) {
if (!this._isTracing) {
this._isTracing = true;
Expand Down
11 changes: 11 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,17 @@ scheme.TracingTracingStartChunkParams = tObject({
scheme.TracingTracingStartChunkResult = tObject({
traceName: tString,
});
scheme.TracingTracingGroupParams = tObject({
name: tString,
location: tOptional(tObject({
file: tString,
line: tOptional(tNumber),
column: tOptional(tNumber),
})),
});
scheme.TracingTracingGroupResult = tOptional(tObject({}));
scheme.TracingTracingGroupEndParams = tOptional(tObject({}));
scheme.TracingTracingGroupEndResult = tOptional(tObject({}));
scheme.TracingTracingStopChunkParams = tObject({
mode: tEnum(['archive', 'discard', 'entries']),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import type * as channels from '@protocol/channels';
import type { CallMetadata } from '@protocol/callMetadata';
import type { Tracing } from '../trace/recorder/tracing';
import { ArtifactDispatcher } from './artifactDispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
Expand All @@ -41,6 +42,15 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
return await this._object.startChunk(params);
}

async tracingGroup(params: channels.TracingTracingGroupParams, metadata: CallMetadata): Promise<channels.TracingTracingGroupResult> {
const { name, location } = params;
await this._object.group(name, location, metadata);
}

async tracingGroupEnd(params: channels.TracingTracingGroupEndParams): Promise<channels.TracingTracingGroupEndResult> {
await this._object.groupEnd();
}

async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
const { artifact, entries } = await this._object.stopChunk(params);
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries };
Expand Down
68 changes: 64 additions & 4 deletions packages/playwright-core/src/server/trace/recorder/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import type { NameValue } from '../../../common/types';
import type { TracingTracingStopChunkParams } from '@protocol/channels';
import type { TracingTracingStopChunkParams, StackFrame } from '@protocol/channels';
import { commandsWithTracingSnapshots } from '../../../protocol/debug';
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
import { Artifact } from '../../artifact';
Expand Down Expand Up @@ -61,6 +61,7 @@ type RecordingState = {
traceSha1s: Set<string>,
recording: boolean;
callIds: Set<string>;
groupStack: string[];
};

const kScreencastOptions = { width: 800, height: 600, quality: 90 };
Expand Down Expand Up @@ -148,6 +149,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
networkSha1s: new Set(),
recording: false,
callIds: new Set(),
groupStack: [],
};
this._fs.mkdir(this._state.resourcesDir);
this._fs.writeFile(this._state.networkFile, '');
Expand Down Expand Up @@ -194,6 +196,53 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { traceName: this._state.traceName };
}

private _currentGroupId(): string | undefined {
return this._state?.groupStack.length ? this._state.groupStack[this._state.groupStack.length - 1] : undefined;
}

async group(name: string, location: { file: string, line?: number, column?: number } | undefined, metadata: CallMetadata): Promise<void> {
if (!this._state)
return;
const stackFrames: StackFrame[] = [];
const { file, line, column } = location ?? metadata.location ?? {};
if (file) {
stackFrames.push({
file,
line: line ?? 0,
column: column ?? 0,
});
}
const event: trace.BeforeActionTraceEvent = {
type: 'before',
callId: metadata.id,
startTime: metadata.startTime,
apiName: name,
class: 'Tracing',
method: 'tracingGroup',
params: { },
stepId: metadata.stepId,
stack: stackFrames,
};
if (this._currentGroupId())
event.parentId = this._currentGroupId();
this._state.groupStack.push(event.callId);
this._appendTraceEvent(event);
}

async groupEnd(): Promise<void> {
if (!this._state)
return;
const callId = this._state.groupStack.pop();
if (!callId)
return;
const event: trace.AfterActionTraceEvent = {
type: 'after',
callId,
endTime: monotonicTime(),
};
this._appendTraceEvent(event);
}

private _startScreencast() {
if (!(this._context instanceof BrowserContext))
return;
Expand Down Expand Up @@ -236,6 +285,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
throw new Error(`Tracing is already stopping`);
if (this._state.recording)
throw new Error(`Must stop trace file before stopping tracing`);
await this._closeAllGroups();
this._harTracer.stop();
this.flushHarEntries();
await this._fs.syncAndGetError();
Expand Down Expand Up @@ -264,6 +314,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
await this._fs.syncAndGetError();
}

async _closeAllGroups() {
while (this._currentGroupId())
await this.groupEnd();
}

async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact?: Artifact, entries?: NameValue[] }> {
if (this._isStopping)
throw new Error(`Tracing is already stopping`);
Expand All @@ -276,6 +331,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return {};
}

await this._closeAllGroups();

this._context.instrumentation.removeListener(this);
eventsHelper.removeEventListeners(this._eventListeners);
if (this._state.options.screenshots)
Expand Down Expand Up @@ -354,7 +411,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps

onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
const event = createBeforeActionTraceEvent(metadata);
const event = createBeforeActionTraceEvent(metadata, this._currentGroupId());
if (!event)
return Promise.resolve();
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
Expand Down Expand Up @@ -571,10 +628,10 @@ export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
}

function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActionTraceEvent | null {
function createBeforeActionTraceEvent(metadata: CallMetadata, parentId?: string): trace.BeforeActionTraceEvent | null {
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
const event: trace.BeforeActionTraceEvent = {
type: 'before',
callId: metadata.id,
startTime: metadata.startTime,
Expand All @@ -585,6 +642,9 @@ function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActio
stepId: metadata.stepId,
pageId: metadata.pageId,
};
if (parentId)
event.parentId = parentId;
return event;
}

function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
Expand Down
56 changes: 56 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21055,6 +21055,62 @@ export interface Touchscreen {
*
*/
export interface Tracing {
/**
* Creates a new inline group within the trace, assigning any subsequent calls to this group until
* [method: Tracing.groupEnd] is invoked.
*
* Groups can be nested and are similar to `test.step` in trace. However, groups are only visualized in the trace
* viewer and, unlike test.step, have no effect on the test reports.
*
* **NOTE** This API is intended for Playwright API users that can not use `test.step`.
*
* **Usage**
*
* ```js
* await context.tracing.start({ screenshots: true, snapshots: true });
* await context.tracing.group('Open Playwright.dev');
* // All actions between group and groupEnd will be shown in the trace viewer as a group.
* const page = await context.newPage();
* await page.goto('https://playwright.dev/');
* await context.tracing.groupEnd();
* await context.tracing.group('Open API Docs of Tracing');
* await page.getByRole('link', { name: 'API' }).click();
* await page.getByRole('link', { name: 'Tracing' }).click();
* await context.tracing.groupEnd();
* // This Trace will have two groups: 'Open Playwright.dev' and 'Open API Docs of Tracing'.
* ```
*
* @param name Group name shown in the actions tree in trace viewer.
* @param options
*/
group(name: string, options?: {
/**
* Specifies a custom location for the group start to be shown in source tab in trace viewer. By default, location of
* the tracing.group() call is shown.
*/
location?: {
/**
* Source file path to be shown in the trace viewer source tab.
*/
file: string;

/**
* Line number in the source file.
*/
line?: number;

/**
* Column number in the source file
*/
column?: number;
};
}): Promise<void>;

/**
* Closes the currently open inline group in the trace.
*/
groupEnd(): Promise<void>;

/**
* Start tracing.
*
Expand Down
12 changes: 10 additions & 2 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions,
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import { rootTestType } from './common/testType';
import type { ContextReuseMode } from './common/config';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
Expand Down Expand Up @@ -255,20 +255,28 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({

const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);

const tracingGroupSteps: TestStepInternal[] = [];
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
if (apiName === 'tracing.groupEnd') {
tracingGroupSteps.pop();
return { userObject: null };
}
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
userData.userObject = step;
out.stepId = step.stepId;
if (apiName === 'tracing.group')
tracingGroupSteps.push(step);
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,15 @@ export class TestInfoImpl implements TestInfo {
}
}

_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps'>): TestStepInternal {
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps'>, parentStep?: TestStepInternal): TestStepInternal {
const stepId = `${data.category}@${++this._lastStepId}`;

let parentStep: TestStepInternal | undefined;
if (data.isStage) {
// Predefined stages form a fixed hierarchy - use the current one as parent.
parentStep = this._findLastStageStep(this._steps);
} else {
parentStep = zones.zoneData<TestStepInternal>('stepZone');
if (!parentStep)
parentStep = zones.zoneData<TestStepInternal>('stepZone');
if (!parentStep) {
// If no parent step on stack, assume the current stage as parent.
parentStep = this._findLastStageStep(this._steps);
Expand Down
Loading

0 comments on commit fa10bcd

Please sign in to comment.