Skip to content

Integration Playwright

aryehcitron@gmail.com edited this page Aug 23, 2026 · 4 revisions

The Kronikol.Playwright package is the browser-driven end-to-end client for Kronikol. It mints a per-test identity, stamps the Kronikol test-tracking-* headers (and a W3C traceparent) on every request a Playwright IBrowserContext / IPage makes, and opens the matching in-process identity scope — so page→backend calls are attributed to the running test.

The sink is downstream, not the fixture. Where the calls get recorded depends on your topology:

Backend Sink What to do
Kronikol-instrumented (.NET, TestTrackingContextMiddleware + TestTrackingMessageHandler) the server Nothing else — the middleware reads the headers and the handlers log under that identity.
Not instrumentable (polyglot, third-party, legacy) a [[proxy tap Integration-ProxyTap-Extension]] on each hop

Install

dotnet add package Kronikol.Playwright

Quick Start (xUnit)

using Kronikol.Playwright;
using Microsoft.Playwright;

public class OverviewTests : IAsyncLifetime
{
    private IPlaywright _pw = null!;
    private IBrowser _browser = null!;

    public async ValueTask InitializeAsync()
    {
        _pw = await Playwright.CreateAsync();
        _browser = await _pw.Chromium.LaunchAsync(new() { Headless = true });
    }

    public async ValueTask DisposeAsync() { await _browser.DisposeAsync(); _pw.Dispose(); }

    [Fact]
    public async Task Overview_renders()
    {
        // One identity per test: name for the report, id = the correlation key
        // (defaults to the identity's W3C trace id, so proxy taps can attribute by traceparent too).
        var identity = TestTrackingIdentity.Create("overview › renders");

        await using var context = await _browser.NewTrackedContextAsync(identity);
        var page = await context.NewPageAsync();
        await page.GotoAsync("http://localhost:4000/intelligence");

        // ... assertions ...
        // Report generation: Scenario.Id must equal identity.TestId.
    }
}

If your test framework already runs under a Kronikol adapter (xUnit/NUnit/MSTest/TUnit/… with TestIdentityScope set), use TestTrackingIdentity.FromCurrentScope() so the browser traffic joins the same scenario as your in-process HttpClient calls.


API

Member Purpose
TestTrackingIdentity.Create(testName, testId?, callerName = "Browser", traceId?) Mint an identity. TestId defaults to TraceId.ToString("N") (32-hex = the W3C trace id).
TestTrackingIdentity.FromCurrentScope() Build from the ambient TestIdentityScope.
identity.ToHeaders() The four TestTrackingHttpHeaders (+ traceparent unless IncludeTraceparent = false). Values are ISO-8859-1-safe and ≤ 512 chars.
browser.NewTrackedContextAsync(identity, options?) New context with the headers merged into ExtraHTTPHeaders. The per-test entry point.
context.UseTestTrackingAsync(identity, additionalHeaders?) / page.UseTestTrackingAsync(...) Stamp an existing context/page (SetExtraHTTPHeadersAsync replaces the set — pass other headers you need via additionalHeaders).
identity.BeginScope() Open the matching in-process TestIdentityScope (dispose to close).
identity.Traceparent() A fresh sampled traceparent rooted at the identity's trace id.

How the headers flow

Browser ──(test-tracking-current-test-name / -current-test-id / -caller-name / -trace-id, traceparent)──►
   your web app ──► graphql ──► myDotnetService ──► …
  • A Kronikol-instrumented hop reads name+id (TestTrackingContextMiddleware) and its outbound handlers re-stamp all four.
  • A ProxyTap does the same from outside: it reads the headers, falls back to the traceparent trace id when a hop dropped them, and re-injects the four headers downstream.
  • Because TestId defaults to the W3C trace id, every hop — instrumented or tapped — lands in the same scenario, and the same id finds the distributed trace in Tempo/Jaeger.

The user in the diagram — actions, steps and assertions (reporter)

Attribution gets the backend calls into the right scenario; the Playwright reporter pattern gets the cause in too. A reporter reads Playwright's own step tree at onTestEnd and writes Kronikol records — no spec changes:

Playwright Kronikol record Rendered as
pw:api actions — Navigate to, Click, Fill, Press, Select option, Hover, … interaction kind: "ui" (callerName: "User", serviceName: "web", method = label, durationMs = until the next action) one arrow from the User actor to the app, with the HTTP calls it caused nested under it
test.step(...) tests NDJSON step (level = nesting depth, status, durationMs, error; Given/When/Then prefix → keyword) black step delimiter bar (top level) / sub-step
expect(...) tests NDJSON assertion (status, error) green ✓ / red ✗ note with the failure message; sub-step of the enclosing step

Labels are trimmed by default (Click "Accept trial", Open /intelligence-pro/overview, "heading" to have text) with Playwright's full title in the note; a raw style keeps the titles verbatim. The reporter finds the test's scenario id in a kronikol-test-id attachment the fixture adds (testInfo.attach('kronikol-test-id', { body: identity.testId })). Hook/fixture steps are skipped.

The reference implementation (TypeScript, ~150 lines) lives in the sidekick-intelligence-e2e repo (tests/kronikol-reporter.ts + the label helpers in tests/kronikol.ts) and is the first concrete piece of the planned @kronikol/playwright package — see NODE_PORT_PLAN.md in the Kronikol repo. Record shapes: Ingesting External Captures.

Screenshots and artefacts

Added in v3.0.45

The same reporter turns Playwright's artefacts into report content. For each entry in result.attachments, append an attachment record to the tests NDJSON — the path Playwright already wrote, plus its content type:

for (const a of result.attachments) {
  if (a.name === 'kronikol-test-id') continue;
  const path = a.path ?? writeInlineBody(a);   // inline bodies land outside the tap directory
  appendTestRecord({ event: 'attachment', testId, name: a.name, path,
                     mediaType: a.contentType, timestamp: new Date().toISOString() });
}

Playwright's own screenshot: 'only-on-failure' and trace/video: 'retain-on-failure' need no extra work — the failure screenshot arrives as image/png and renders inline with a lightbox, trace.zip and video.webm render as links. Start/end screenshots are an auto fixture away:

const screenshots = [async ({ page }, use, testInfo) => {
  page.once('load', () => page.screenshot().then(
    body => testInfo.attach('screenshot-start.png', { body, contentType: 'image/png' })).catch(() => {}));
  await use();
  if (!page.isClosed())
    await page.screenshot().then(
      body => testInfo.attach('screenshot-end.png', { body, contentType: 'image/png' })).catch(() => {});
}, { auto: true }] as const;

Two rules the fixture must obey: capture at the first load (at fixture setup the page is still about:blank), and swallow every error — nothing in a reporting fixture may fail a test. Under playwright-bdd, AfterStep attaches one image per Gherkin step; Playwright ≥ 1.50 files attachments made inside a step on that step, and Kronikol's step index puts them on the matching step row.

Links belong here too: a path that is a http/https URL is rendered as a plain link and never copied — that is how a "Playwright report" or "Grafana trace" entry gets onto every scenario.

kronikol ingest --attachments-base <dir> resolves relative paths; --clean-attachments empties attachments/ first so the folder holds exactly this run's files. Full contract: Ingesting External Captures.

.NET parity note: Playwright .NET has no reporter/step API, so there is no automatic equivalent here; a .NET suite that wants user actions in its diagrams records them explicitly — e.g. log an InteractionRecord.UserAction(...) (or a RequestResponseLog { IsUserAction = true }) around the page calls it cares about, and use TrackingDiagramOverride.InsertPlantUml / Track.That(...) for steps and assertions as any .NET test does.

playwright-bdd — Gherkin features, and the structure comes for free

playwright-bdd runs .feature files on Playwright: bddgen generates a spec per feature, each Gherkin step becomes a test.step titled "<Keyword> text", tags become Playwright tags, a Rule becomes a describe, a Background a beforeEach and an outline one test per example row. The reporter pattern above already gets bars and notes out of that — but playwright-bdd can also emit the Cucumber Messages protocol, which carries the whole Gherkin model, and Kronikol reads it directly.

// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { defineBddConfig, cucumberReporter } from 'playwright-bdd';

const testDir = defineBddConfig({
  features: 'features/**/*.feature',
  steps: 'steps/**/*.ts',
  outputDir: '.features-gen',          // gitignore this
});

export default defineConfig({
  testDir,
  reporter: [
    ['./kronikol-reporter.ts'],                                            // ui / assertion / identity
    cucumberReporter('message', { outputFile: '../.logs/cucumber/messages.ndjson' }),
  ],
});

Run npx bddgen before npx playwright test, then ingest both files:

kronikol ingest .logs/taps --tests .logs/taps/tests.jsonl   --cucumber-messages .logs/cucumber/messages.ndjson -o .logs/kronikol

Two rules make this work:

  • Write the messages file outside the capture directory. kronikol ingest (and IngestPipeline) scan input directories recursively for *.ndjson/*.jsonl and would try to replay a messages file as traffic.

  • Attach the identity. A Before hook that attaches the 32-hex Kronikol test id as kronikol-test-id is what joins the Gherkin scenario to the traffic the fixture captured — the same attachment the reporter reads:

    const { Before } = createBdd(test);
    Before(async ({ $testInfo }) => {
      await $testInfo.attach('kronikol-test-id', { body: identity.testId, contentType: 'text/plain' });
    });

With both sources present the messages win for structure and the reporter still supplies assertions, UI actions and attachments; with the messages file absent the reporter alone still produces keyword bars and ✓/✗ notes, so the recipe degrades cleanly. Full mapping: Integration Cucumber Messages.

Other languages

The header names and the "test mints the trace, trace id = test id" convention are the contract for the planned @kronikol/playwright (Node) fixture and the Java port — a TypeScript Playwright fixture needs only context.setExtraHTTPHeaders({...}) with the same five headers. See Ingesting External Captures for the capture format those ports share.

Home


Demo


Getting Started

Common Tasks

Integration Guides

Uninstrumentable / polyglot backends

Extensions

Configuration

Features

Reference

Clone this wiki locally