@heal-dev/heal-playwright-tracer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +245 -0
  3. package/dist/application/babel-playwright-tracer-plugin/index.d.ts +16 -0
  4. package/dist/application/babel-playwright-tracer-plugin/index.js +111 -0
  5. package/dist/application/heal-config/index.d.ts +8 -0
  6. package/dist/application/heal-config/index.js +22 -0
  7. package/dist/application/heal-config/registry.d.ts +37 -0
  8. package/dist/application/heal-config/registry.js +64 -0
  9. package/dist/application/heal-config/types.d.ts +73 -0
  10. package/dist/application/heal-config/types.js +7 -0
  11. package/dist/application/playwright-fixture/index.d.ts +14 -0
  12. package/dist/application/playwright-fixture/index.js +234 -0
  13. package/dist/application/trace-event-recorder-runtime/index.d.ts +15 -0
  14. package/dist/application/trace-event-recorder-runtime/index.js +68 -0
  15. package/dist/domain/code-hook-injector/service/meta-fields/enclosing-scope-labeler.d.ts +12 -0
  16. package/dist/domain/code-hook-injector/service/meta-fields/enclosing-scope-labeler.js +53 -0
  17. package/dist/domain/code-hook-injector/service/meta-fields/leading-comment-extractor.d.ts +14 -0
  18. package/dist/domain/code-hook-injector/service/meta-fields/leading-comment-extractor.js +20 -0
  19. package/dist/domain/code-hook-injector/service/meta-fields/relative-file-path.d.ts +6 -0
  20. package/dist/domain/code-hook-injector/service/meta-fields/relative-file-path.js +57 -0
  21. package/dist/domain/code-hook-injector/service/meta-fields/source-snippet-extractor.d.ts +12 -0
  22. package/dist/domain/code-hook-injector/service/meta-fields/source-snippet-extractor.js +28 -0
  23. package/dist/domain/code-hook-injector/service/playwright-import-rewriter.d.ts +10 -0
  24. package/dist/domain/code-hook-injector/service/playwright-import-rewriter.js +21 -0
  25. package/dist/domain/code-hook-injector/service/statement-analysis/cjs-artifact-detector.d.ts +14 -0
  26. package/dist/domain/code-hook-injector/service/statement-analysis/cjs-artifact-detector.js +30 -0
  27. package/dist/domain/code-hook-injector/service/statement-analysis/for-head-declaration-detector.d.ts +10 -0
  28. package/dist/domain/code-hook-injector/service/statement-analysis/for-head-declaration-detector.js +18 -0
  29. package/dist/domain/code-hook-injector/service/statement-analysis/leaf-statement-classifier.d.ts +15 -0
  30. package/dist/domain/code-hook-injector/service/statement-analysis/leaf-statement-classifier.js +66 -0
  31. package/dist/domain/code-hook-injector/service/statement-analysis/non-wrappable-statement.d.ts +11 -0
  32. package/dist/domain/code-hook-injector/service/statement-analysis/non-wrappable-statement.js +20 -0
  33. package/dist/domain/code-hook-injector/service/trace-hook/enter-meta-literal.d.ts +20 -0
  34. package/dist/domain/code-hook-injector/service/trace-hook/enter-meta-literal.js +34 -0
  35. package/dist/domain/code-hook-injector/service/trace-hook/global-trace-call.d.ts +10 -0
  36. package/dist/domain/code-hook-injector/service/trace-hook/global-trace-call.js +16 -0
  37. package/dist/domain/code-hook-injector/service/trace-hook/try-finally-wrapper.d.ts +18 -0
  38. package/dist/domain/code-hook-injector/service/trace-hook/try-finally-wrapper.js +44 -0
  39. package/dist/domain/code-hook-injector/service/trace-hook/variable-declaration-hoister.d.ts +20 -0
  40. package/dist/domain/code-hook-injector/service/trace-hook/variable-declaration-hoister.js +27 -0
  41. package/dist/domain/code-hook-injector/service/traced-file-matcher.d.ts +10 -0
  42. package/dist/domain/code-hook-injector/service/traced-file-matcher.js +26 -0
  43. package/dist/domain/trace-event-recorder/model/enter-meta.d.ts +24 -0
  44. package/dist/domain/trace-event-recorder/model/enter-meta.js +7 -0
  45. package/dist/domain/trace-event-recorder/model/global-names.d.ts +8 -0
  46. package/dist/domain/trace-event-recorder/model/global-names.js +20 -0
  47. package/dist/domain/trace-event-recorder/model/serialized-error.d.ts +16 -0
  48. package/dist/domain/trace-event-recorder/model/serialized-error.js +7 -0
  49. package/dist/domain/trace-event-recorder/model/statement-trace-schema.d.ts +171 -0
  50. package/dist/domain/trace-event-recorder/model/statement-trace-schema.js +33 -0
  51. package/dist/domain/trace-event-recorder/model/trace-schema.d.ts +114 -0
  52. package/dist/domain/trace-event-recorder/model/trace-schema.js +16 -0
  53. package/dist/domain/trace-event-recorder/port/clock.d.ts +9 -0
  54. package/dist/domain/trace-event-recorder/port/clock.js +7 -0
  55. package/dist/domain/trace-event-recorder/port/heal-trace-exporter.d.ts +11 -0
  56. package/dist/domain/trace-event-recorder/port/heal-trace-exporter.js +7 -0
  57. package/dist/domain/trace-event-recorder/port/system-info-provider.d.ts +18 -0
  58. package/dist/domain/trace-event-recorder/port/system-info-provider.js +7 -0
  59. package/dist/domain/trace-event-recorder/port/trace-event-consumer.d.ts +11 -0
  60. package/dist/domain/trace-event-recorder/port/trace-event-consumer.js +7 -0
  61. package/dist/domain/trace-event-recorder/service/active-enter-stack.d.ts +15 -0
  62. package/dist/domain/trace-event-recorder/service/active-enter-stack.js +34 -0
  63. package/dist/domain/trace-event-recorder/service/event-builders/enter-event-builder.d.ts +8 -0
  64. package/dist/domain/trace-event-recorder/service/event-builders/enter-event-builder.js +37 -0
  65. package/dist/domain/trace-event-recorder/service/event-builders/meta-event-builder.d.ts +7 -0
  66. package/dist/domain/trace-event-recorder/service/event-builders/meta-event-builder.js +19 -0
  67. package/dist/domain/trace-event-recorder/service/event-builders/ok-event-builder.d.ts +7 -0
  68. package/dist/domain/trace-event-recorder/service/event-builders/ok-event-builder.js +27 -0
  69. package/dist/domain/trace-event-recorder/service/event-builders/throw-event-builder.d.ts +7 -0
  70. package/dist/domain/trace-event-recorder/service/event-builders/throw-event-builder.js +23 -0
  71. package/dist/domain/trace-event-recorder/service/exporters/composite-heal-trace-exporter.d.ts +12 -0
  72. package/dist/domain/trace-event-recorder/service/exporters/composite-heal-trace-exporter.js +32 -0
  73. package/dist/domain/trace-event-recorder/service/index.d.ts +10 -0
  74. package/dist/domain/trace-event-recorder/service/index.js +15 -0
  75. package/dist/domain/trace-event-recorder/service/projectors/index.d.ts +6 -0
  76. package/dist/domain/trace-event-recorder/service/projectors/index.js +10 -0
  77. package/dist/domain/trace-event-recorder/service/projectors/statement-projector.d.ts +26 -0
  78. package/dist/domain/trace-event-recorder/service/projectors/statement-projector.js +183 -0
  79. package/dist/domain/trace-event-recorder/service/serializers/error-serializer.d.ts +8 -0
  80. package/dist/domain/trace-event-recorder/service/serializers/error-serializer.js +49 -0
  81. package/dist/domain/trace-event-recorder/service/serializers/variable-snapshot-serializer.d.ts +7 -0
  82. package/dist/domain/trace-event-recorder/service/serializers/variable-snapshot-serializer.js +102 -0
  83. package/dist/domain/trace-event-recorder/service/trace-event-recorder-state.d.ts +19 -0
  84. package/dist/domain/trace-event-recorder/service/trace-event-recorder-state.js +7 -0
  85. package/dist/domain/trace-event-recorder/service/trace-event-recorder.d.ts +56 -0
  86. package/dist/domain/trace-event-recorder/service/trace-event-recorder.js +80 -0
  87. package/dist/index.d.ts +11 -0
  88. package/dist/index.js +43 -0
  89. package/dist/infrastructure/ndjson-exporter-adapter/index.d.ts +6 -0
  90. package/dist/infrastructure/ndjson-exporter-adapter/index.js +10 -0
  91. package/dist/infrastructure/ndjson-exporter-adapter/ndjson-exporter.d.ts +13 -0
  92. package/dist/infrastructure/ndjson-exporter-adapter/ndjson-exporter.js +77 -0
  93. package/dist/infrastructure/perf-hooks-clock-adapter/index.d.ts +6 -0
  94. package/dist/infrastructure/perf-hooks-clock-adapter/index.js +10 -0
  95. package/dist/infrastructure/perf-hooks-clock-adapter/perf-hooks-clock.d.ts +11 -0
  96. package/dist/infrastructure/perf-hooks-clock-adapter/perf-hooks-clock.js +22 -0
  97. package/dist/infrastructure/playwright-locator-screenshot-adapter/assertion-wrapper.d.ts +6 -0
  98. package/dist/infrastructure/playwright-locator-screenshot-adapter/assertion-wrapper.js +109 -0
  99. package/dist/infrastructure/playwright-locator-screenshot-adapter/index.d.ts +9 -0
  100. package/dist/infrastructure/playwright-locator-screenshot-adapter/index.js +21 -0
  101. package/dist/infrastructure/playwright-locator-screenshot-adapter/locator-patch.d.ts +11 -0
  102. package/dist/infrastructure/playwright-locator-screenshot-adapter/locator-patch.js +79 -0
  103. package/dist/infrastructure/playwright-locator-screenshot-adapter/overlay-helpers.d.ts +15 -0
  104. package/dist/infrastructure/playwright-locator-screenshot-adapter/overlay-helpers.js +33 -0
  105. package/dist/infrastructure/playwright-locator-screenshot-adapter/screenshot-capture-session.d.ts +26 -0
  106. package/dist/infrastructure/playwright-locator-screenshot-adapter/screenshot-capture-session.js +125 -0
  107. package/dist/infrastructure/playwright-step-tracking-adapter/index.d.ts +7 -0
  108. package/dist/infrastructure/playwright-step-tracking-adapter/index.js +10 -0
  109. package/dist/infrastructure/playwright-step-tracking-adapter/playwright-step-tracking-adapter.d.ts +14 -0
  110. package/dist/infrastructure/playwright-step-tracking-adapter/playwright-step-tracking-adapter.js +51 -0
  111. package/dist/infrastructure/playwright-test-context-adapter/heal-tag-prefix.d.ts +25 -0
  112. package/dist/infrastructure/playwright-test-context-adapter/heal-tag-prefix.js +28 -0
  113. package/dist/infrastructure/playwright-test-context-adapter/index.d.ts +8 -0
  114. package/dist/infrastructure/playwright-test-context-adapter/index.js +12 -0
  115. package/dist/infrastructure/playwright-test-context-adapter/playwright-test-context-adapter.d.ts +19 -0
  116. package/dist/infrastructure/playwright-test-context-adapter/playwright-test-context-adapter.js +43 -0
  117. package/dist/infrastructure/stdout-capture-adapter/index.d.ts +7 -0
  118. package/dist/infrastructure/stdout-capture-adapter/index.js +10 -0
  119. package/dist/infrastructure/stdout-capture-adapter/stdout-capture-session.d.ts +20 -0
  120. package/dist/infrastructure/stdout-capture-adapter/stdout-capture-session.js +47 -0
  121. package/dist/infrastructure/system-info-adapter/index.d.ts +6 -0
  122. package/dist/infrastructure/system-info-adapter/index.js +10 -0
  123. package/dist/infrastructure/system-info-adapter/system-info-adapter.d.ts +12 -0
  124. package/dist/infrastructure/system-info-adapter/system-info-adapter.js +83 -0
  125. package/package.json +95 -0
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ <h1 align="center">
2
+ <a href="https://heal.dev/">
3
+ <img width="240" src="assets/heal-logo.svg" alt="heal">
4
+ </a>
5
+ </h1>
6
+ <p align="center">
7
+ <p align="center">Statement-level execution tracing for Playwright tests, purpose-built for AI autopilots.</p>
8
+ </p>
9
+
10
+ <h4 align="center">
11
+ <a href="https://app.heal.dev/">SaaS</a> |
12
+ <a href="https://heal.dev/">Website</a> |
13
+ <a href="https://docs.heal.dev/">Docs</a>
14
+ </h4>
15
+
16
+ <h4 align="center">
17
+ <a href="https://github.com/heal-dev/heal-playwright-tracer/actions/workflows/ci.yml"><img src="https://github.com/heal-dev/heal-playwright-tracer/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
18
+ <a href="https://codecov.io/gh/heal-dev/heal-playwright-tracer"><img src="https://codecov.io/gh/heal-dev/heal-playwright-tracer/branch/main/graph/badge.svg" alt="codecov"></a>
19
+ <a href="https://www.npmjs.com/package/@heal-dev/heal-playwright-tracer"><img src="https://img.shields.io/npm/v/@heal-dev/heal-playwright-tracer.svg" alt="npm version"></a>
20
+ <a href="https://www.npmjs.com/package/@heal-dev/heal-playwright-tracer"><img src="https://img.shields.io/node/v/@heal-dev/heal-playwright-tracer.svg" alt="node"></a>
21
+ <a href="https://www.npmjs.com/package/@playwright/test"><img src="https://img.shields.io/npm/dependency-version/@heal-dev/heal-playwright-tracer/peer/@playwright/test" alt="playwright"></a>
22
+ </h4>
23
+
24
+ # @heal-dev/heal-playwright-tracer
25
+
26
+ An AI-agent-first diagnostic layer for Playwright tests. Purpose-built
27
+ to give an autopilot agent everything it needs to reason about _why_
28
+ a test failed — statement-level execution traces with timing,
29
+ variable values, call depth, serialized errors, highlighted locator
30
+ screenshots, and Playwright API correlations — emitted as a
31
+ structured NDJSON stream per test, alongside Playwright's own HTML
32
+ report and trace viewer. Useful to humans too, but every design
33
+ decision optimizes for what an LLM needs to see.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ npm install -D @heal-dev/heal-playwright-tracer
39
+ ```
40
+
41
+ Wire the Babel plugin in `playwright.config.ts`.
42
+
43
+ ```ts
44
+ // playwright.config.ts
45
+ import { defineConfig } from '@playwright/test';
46
+
47
+ export default defineConfig({
48
+ // @ts-ignore — `babelPlugins` is a supported Playwright option not yet in its public types
49
+ '@playwright/test': {
50
+ babelPlugins: [
51
+ [
52
+ require.resolve('@heal-dev/heal-playwright-tracer/code-hook-injector'),
53
+ { include: [/\/tests\//] },
54
+ ],
55
+ ],
56
+ },
57
+ });
58
+ ```
59
+
60
+ Or, if you prefer to keep the config fully typed, declare the
61
+ option once at the top of the file instead of using `@ts-ignore`:
62
+
63
+ ```ts
64
+ declare module '@playwright/test' {
65
+ interface Config {
66
+ '@playwright/test'?: {
67
+ babelPlugins?: Array<[string, object?]>;
68
+ };
69
+ }
70
+ }
71
+ ```
72
+
73
+ Per-test output lands at
74
+ `test-results/<test>/heal-data/heal-traces.ndjson`.
75
+
76
+ ### Extend: custom exporters and lifecycles
77
+
78
+ `configureTracer` registers extra exporters (fanned out alongside
79
+ the default NDJSON exporter) and per-test setup/teardown pairs —
80
+ useful for shipping traces to your own backend or installing
81
+ per-test globals:
82
+
83
+ ```ts
84
+ // playwright.config.ts
85
+ import { defineConfig } from '@playwright/test';
86
+ import { configureTracer } from '@heal-dev/heal-playwright-tracer';
87
+
88
+ configureTracer({
89
+ exporters: [(ctx) => new MyHttpExporter(ctx.transport)],
90
+ lifecycles: [
91
+ () => ({
92
+ setup: (ctx) => openTelemetrySession(ctx.testInfo),
93
+ teardown: () => closeTelemetrySession(),
94
+ }),
95
+ ],
96
+ });
97
+
98
+ export default defineConfig({
99
+ /* ... */
100
+ });
101
+ ```
102
+
103
+ Full surface: [`src/application/heal-config/types.ts`](src/application/heal-config/types.ts).
104
+ Exporters implement [`HealTraceExporter`](src/domain/trace-event-recorder/port/heal-trace-exporter.ts)
105
+ (`write(record)` + `close()`).
106
+
107
+ ## Sample output
108
+
109
+ `heal-data/heal-traces.ndjson` — one record per line:
110
+
111
+ ```ndjson
112
+ {"kind":"test-header","schemaVersion":1,"test":{"title":"it works","file":"tests/example.spec.ts","context":{"testId":"...","attempt":1}}}
113
+ {"kind":"statement","statement":{"loc":{"line":5},"source":"await page.goto('https://example.com')","durationMs":412,"status":"ok","children":[...]}}
114
+ {"kind":"statement","statement":{"loc":{"line":6},"source":"await expect(page.getByRole('heading')).toBeVisible()","durationMs":73,"status":"ok"}}
115
+ {"kind":"test-result","status":"passed","duration":1234,"stdout":"...","stderr":""}
116
+ ```
117
+
118
+ Schema: [`src/domain/trace-event-recorder/model/statement-trace-schema.ts`](src/domain/trace-event-recorder/model/statement-trace-schema.ts)
119
+ (also exported as `@heal-dev/heal-playwright-tracer/statement-trace-schema`).
120
+
121
+ ### Screenshots
122
+
123
+ Every statement that calls a patched Playwright **locator action**
124
+ (`click`, `fill`, `hover`, `press`, …) or a **locator assertion**
125
+ (`expect(locator).toBeVisible()`, `toHaveText()`, …) produces a
126
+ PNG screenshot with the targeted element outlined via an overlay
127
+ drawn in-page — so the agent sees _what Playwright was actually
128
+ pointing at_ at the moment the action ran, not just the raw page.
129
+
130
+ Files are written to the per-test `heal-data/` directory and
131
+ referenced on the corresponding statement via the `screenshot`
132
+ field:
133
+
134
+ ```ndjson
135
+ {"kind":"statement","statement":{"source":"await page.getByRole('button', { name: 'Submit' }).click()","status":"ok","screenshot":"stmt-0007.png"}}
136
+ {"kind":"statement","statement":{"source":"await expect(page.getByRole('alert')).toBeVisible()","status":"ok","screenshot":"stmt-0008.png"}}
137
+ ```
138
+
139
+ Statements that don't touch a locator (plain JS, utility calls,
140
+ `page.goto`) have no `screenshot` field — capture is scoped to the
141
+ Playwright surface where it adds diagnostic signal.
142
+
143
+ ## How it works
144
+
145
+ ```
146
+ Build time (per worker) Runtime (per test)
147
+ ─────────────────────── ──────────────────
148
+
149
+ test file instrumented test
150
+ │ │
151
+ ▼ ▼
152
+ ┌───────────────────┐ ┌────────────────┐
153
+ │ Babel plugin │ ─── instrumented ───► │ recorder │
154
+ │ code-hook- │ (__enter / │ enter/ok/ │
155
+ │ injector │ __ok / __throw) │ throw stream │
156
+ └───────────────────┘ └────────┬───────┘
157
+
158
+
159
+ ┌────────────────┐
160
+ │ statement │
161
+ │ projector │
162
+ └────────┬───────┘
163
+
164
+
165
+ playwright.config.ts ┌──────────────────┐
166
+ configureTracer({ ─── extends ──────────► │ composite │
167
+ exporters, │ exporter │
168
+ lifecycles, └───┬──────────┬───┘
169
+ }) │ │
170
+ ▼ ▼
171
+ NDJSON custom
172
+ file exporters
173
+ (HTTP, queue, …)
174
+ ```
175
+
176
+ The Babel plugin wraps every leaf statement with a try/catch/finally
177
+ that calls three runtime hooks. The recorder pairs those calls into an
178
+ event stream, the projector folds them into `HealTraceRecord`s, and
179
+ a composite exporter fans them out to the default NDJSON file and
180
+ any exporters registered via `configureTracer`.
181
+
182
+ The plugin also rewrites `from '@playwright/test'` to
183
+ `from '@heal-dev/heal-playwright-tracer'` in every instrumented file,
184
+ so `test` and `expect` automatically resolve to the traced variants —
185
+ no manual import swap required.
186
+
187
+ ## Why CommonJS?
188
+
189
+ The package ships as CommonJS (no `"type": "module"` in
190
+ `package.json`, `tsc` emits `module: commonjs`). This is deliberate:
191
+ Playwright's babel transform — the thing that actually loads
192
+ `code-hook-injector` — is itself a CJS module and consumes the plugin
193
+ via `require()`. Shipping ESM would force a dual build with no upside.
194
+
195
+ ESM consumers still work — use `createRequire` in
196
+ `playwright.config.ts` if you need to resolve the plugin path:
197
+
198
+ ```ts
199
+ // playwright.config.ts (package.json has "type": "module")
200
+ import { defineConfig } from '@playwright/test';
201
+ import { createRequire } from 'node:module';
202
+
203
+ const require = createRequire(import.meta.url);
204
+
205
+ export default defineConfig({
206
+ // @ts-ignore
207
+ '@playwright/test': {
208
+ babelPlugins: [[require.resolve('@heal-dev/heal-playwright-tracer/code-hook-injector')]],
209
+ },
210
+ });
211
+ ```
212
+
213
+ > **The module format of `playwright.config.ts` must match the
214
+ > `"type"` field of its nearest `package.json`.** A mismatch causes
215
+ > Node to route the file through the wrong loader, typically surfacing
216
+ > as `ReferenceError: exports is not defined in ES module scope` —
217
+ > with a stack trace that blames this plugin even though it has never
218
+ > run. If that happens, fix the config format first.
219
+
220
+ ## Caveats
221
+
222
+ The Babel plugin rewrites every leaf statement with a `try/catch/finally`
223
+ and three hook calls — the same shape of transformation Istanbul applies
224
+ for code coverage. Two consequences to be aware of:
225
+
226
+ - **Instrumented files are larger.** Each statement gains a wrapper, so
227
+ on-disk size of transformed test files grows noticeably (typically
228
+ ~2–4×, depending on statement density). This affects the files
229
+ Playwright loads into workers, not your application bundle.
230
+ - **Tests run slightly slower.** The per-statement hook overhead is
231
+ small in absolute terms but not free — expect a modest slowdown on
232
+ CPU-bound test code. I/O-bound tests (the common case: `await
233
+ page.click(...)`, network, navigation) are dominated by the browser
234
+ and barely move.
235
+
236
+ Scope the `include` filter in `playwright.config.ts` so only your
237
+ `tests/` directory is instrumented — never your app code or
238
+ `node_modules` — to keep the cost contained.
239
+
240
+ ## License
241
+
242
+ Copyright © 2026 **MYIA SAS**.
243
+
244
+ This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**.
245
+ See the [LICENSE](LICENSE) file for the full text.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright: (c) Myia SAS 2026.
3
+ * This file and its contents are licensed under the AGPLv3 License.
4
+ * Please see the LICENSE file at the root of this repository
5
+ */
6
+ import type * as BabelTypes from '@babel/types';
7
+ import type { PluginObj, PluginPass } from '@babel/core';
8
+ import { type Include } from '../../domain/code-hook-injector/service/traced-file-matcher';
9
+ interface CodeHookInjectorOptions {
10
+ include?: Include;
11
+ rootDir?: string;
12
+ }
13
+ declare function codeHookInjector(api: {
14
+ types: typeof BabelTypes;
15
+ }, opts?: CodeHookInjectorOptions): PluginObj<PluginPass>;
16
+ export = codeHookInjector;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright: (c) Myia SAS 2026.
4
+ * This file and its contents are licensed under the AGPLv3 License.
5
+ * Please see the LICENSE file at the root of this repository
6
+ */
7
+ const traced_file_matcher_1 = require("../../domain/code-hook-injector/service/traced-file-matcher");
8
+ const cjs_artifact_detector_1 = require("../../domain/code-hook-injector/service/statement-analysis/cjs-artifact-detector");
9
+ const leaf_statement_classifier_1 = require("../../domain/code-hook-injector/service/statement-analysis/leaf-statement-classifier");
10
+ const non_wrappable_statement_1 = require("../../domain/code-hook-injector/service/statement-analysis/non-wrappable-statement");
11
+ const for_head_declaration_detector_1 = require("../../domain/code-hook-injector/service/statement-analysis/for-head-declaration-detector");
12
+ const enclosing_scope_labeler_1 = require("../../domain/code-hook-injector/service/meta-fields/enclosing-scope-labeler");
13
+ const source_snippet_extractor_1 = require("../../domain/code-hook-injector/service/meta-fields/source-snippet-extractor");
14
+ const leading_comment_extractor_1 = require("../../domain/code-hook-injector/service/meta-fields/leading-comment-extractor");
15
+ const relative_file_path_1 = require("../../domain/code-hook-injector/service/meta-fields/relative-file-path");
16
+ const playwright_import_rewriter_1 = require("../../domain/code-hook-injector/service/playwright-import-rewriter");
17
+ const global_trace_call_1 = require("../../domain/code-hook-injector/service/trace-hook/global-trace-call");
18
+ const enter_meta_literal_1 = require("../../domain/code-hook-injector/service/trace-hook/enter-meta-literal");
19
+ const try_finally_wrapper_1 = require("../../domain/code-hook-injector/service/trace-hook/try-finally-wrapper");
20
+ const variable_declaration_hoister_1 = require("../../domain/code-hook-injector/service/trace-hook/variable-declaration-hoister");
21
+ const global_names_1 = require("../../domain/trace-event-recorder/model/global-names");
22
+ function codeHookInjector(api, opts = {}) {
23
+ const t = api.types;
24
+ const CWD = opts.rootDir || process.cwd();
25
+ const matches = (0, traced_file_matcher_1.buildMatcher)(opts.include);
26
+ const { isGeneratedModuleStatement } = (0, cjs_artifact_detector_1.createCjsArtifactDetector)(t);
27
+ const { isLeafStatement, kindOf, containsAwait } = (0, leaf_statement_classifier_1.createLeafStatementClassifier)(t);
28
+ const findScopeName = (0, enclosing_scope_labeler_1.createEnclosingScopeLabeler)(t);
29
+ const isNonWrappableStatement = (0, non_wrappable_statement_1.createNonWrappableStatementPredicate)(t, isGeneratedModuleStatement);
30
+ const isForHeadDeclaration = (0, for_head_declaration_detector_1.createForHeadDeclarationDetector)(t);
31
+ const rewritePlaywrightImports = (0, playwright_import_rewriter_1.createPlaywrightImportRewriter)(t);
32
+ const callStmt = (0, global_trace_call_1.createGlobalTraceCallBuilder)(t);
33
+ const buildMeta = (0, enter_meta_literal_1.createEnterMetaLiteralBuilder)(t, {
34
+ kindOf,
35
+ containsAwait,
36
+ findScopeName,
37
+ extractSource: source_snippet_extractor_1.extractSource,
38
+ extractLeadingComment: leading_comment_extractor_1.extractLeadingComment,
39
+ relFile: relative_file_path_1.relFile,
40
+ });
41
+ const { buildTryFinally, buildThrewDecl } = (0, try_finally_wrapper_1.createTryFinallyWrapperBuilder)(t, callStmt);
42
+ const hoistVariableDeclaration = (0, variable_declaration_hoister_1.createVariableDeclarationHoister)(t);
43
+ return {
44
+ name: 'heal-playwright-tracer',
45
+ visitor: {
46
+ // Rewrite `@playwright/test` → `@heal-dev/heal-playwright-tracer` at
47
+ // the top of every matched file. Runs before the Statement visitor
48
+ // because Program.enter fires first. The user keeps the standard
49
+ // Playwright import; our auto-fixture loads transparently.
50
+ Program(programPath, state) {
51
+ if (!matches(state.file.opts.filename || ''))
52
+ return;
53
+ rewritePlaywrightImports(programPath.node);
54
+ },
55
+ Statement(stmtPath, state) {
56
+ const node = stmtPath.node;
57
+ // Don't recurse into our own generated wrapper.
58
+ if (node._traced)
59
+ return;
60
+ // Babel sometimes hands us synthetic nodes without source locations.
61
+ if (!node.loc)
62
+ return;
63
+ // File-level filter: only touch files the consumer opted in to.
64
+ if (!matches(state.file.opts.filename || ''))
65
+ return;
66
+ // Skip statement kinds that no hook family should wrap.
67
+ if (isNonWrappableStatement(stmtPath))
68
+ return;
69
+ // Only leaf statements get wrapped. Compound statements
70
+ // (if/for/while/switch/try) are transparent — their inner
71
+ // leaves will be visited independently.
72
+ if (!isLeafStatement(node))
73
+ return;
74
+ // From here on it's trace-hook-specific. When a second hook
75
+ // family lands, extract the block below into a
76
+ // `applyTraceHookIfLeaf(stmtPath, state)` function in
77
+ // ./trace-hook/ and call it alongside the future families.
78
+ const meta = buildMeta(state, stmtPath, node, CWD);
79
+ if (t.isVariableDeclaration(node)) {
80
+ // `const x = EXPR` can't be wrapped as-is — the binding
81
+ // would be scoped to the try block and invisible downstream.
82
+ // Hoist the bindings out, assign inside the try, pass a
83
+ // vars object to __ok so the recorder can snapshot values.
84
+ if (isForHeadDeclaration(node, stmtPath.parent))
85
+ return;
86
+ const { hoistDecl, assignments, varsObject, bindingNames } = hoistVariableDeclaration(node);
87
+ const okArgs = bindingNames.size ? [varsObject] : [];
88
+ const { threwId, tryStmt } = buildTryFinally(stmtPath.scope, assignments, okArgs);
89
+ stmtPath.replaceWithMultiple([
90
+ callStmt(global_names_1.HEAL_ENTER, [meta]),
91
+ hoistDecl,
92
+ buildThrewDecl(threwId),
93
+ tryStmt,
94
+ ]);
95
+ return;
96
+ }
97
+ // Every other leaf statement: wrap in a block with the
98
+ // original statement inside the try body.
99
+ node._traced = true;
100
+ const { threwId, tryStmt } = buildTryFinally(stmtPath.scope, [node]);
101
+ const wrapper = t.blockStatement([
102
+ callStmt(global_names_1.HEAL_ENTER, [meta]),
103
+ buildThrewDecl(threwId),
104
+ tryStmt,
105
+ ]);
106
+ stmtPath.replaceWith(wrapper);
107
+ },
108
+ },
109
+ };
110
+ }
111
+ module.exports = codeHookInjector;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright: (c) Myia SAS 2026.
3
+ * This file and its contents are licensed under the AGPLv3 License.
4
+ * Please see the LICENSE file at the root of this repository
5
+ */
6
+ export { configureTracer, getTracerConfig, onTestTeardown } from './registry';
7
+ export { resetTeardownHooks, drainTeardownHooks } from './registry';
8
+ export type { HealTracerConfig, HealTracerTestContext, HealTraceExporterFactory, HealTestLifecycle, HealTestLifecycleFactory, } from './types';
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright: (c) Myia SAS 2026.
4
+ * This file and its contents are licensed under the AGPLv3 License.
5
+ * Please see the LICENSE file at the root of this repository
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.drainTeardownHooks = exports.resetTeardownHooks = exports.onTestTeardown = exports.getTracerConfig = exports.configureTracer = void 0;
9
+ // Public surface of the tracer's extension API.
10
+ //
11
+ // Re-exports what a user needs to extend the tracer from their own
12
+ // `playwright.config.ts`.
13
+ var registry_1 = require("./registry");
14
+ Object.defineProperty(exports, "configureTracer", { enumerable: true, get: function () { return registry_1.configureTracer; } });
15
+ Object.defineProperty(exports, "getTracerConfig", { enumerable: true, get: function () { return registry_1.getTracerConfig; } });
16
+ Object.defineProperty(exports, "onTestTeardown", { enumerable: true, get: function () { return registry_1.onTestTeardown; } });
17
+ // Internal-facing exports — consumed by the fixture only. Kept in the
18
+ // barrel because the fixture imports from this same file; external
19
+ // callers have no reason to touch them.
20
+ var registry_2 = require("./registry");
21
+ Object.defineProperty(exports, "resetTeardownHooks", { enumerable: true, get: function () { return registry_2.resetTeardownHooks; } });
22
+ Object.defineProperty(exports, "drainTeardownHooks", { enumerable: true, get: function () { return registry_2.drainTeardownHooks; } });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Copyright: (c) Myia SAS 2026.
3
+ * This file and its contents are licensed under the AGPLv3 License.
4
+ * Please see the LICENSE file at the root of this repository
5
+ */
6
+ import type { HealTracerConfig } from './types';
7
+ /**
8
+ * Register the tracer's extension config. Typically called once at
9
+ * the top of `playwright.config.ts`, before `defineConfig(...)`. Later
10
+ * calls overwrite earlier ones — there is no merge.
11
+ */
12
+ export declare function configureTracer(config: HealTracerConfig): void;
13
+ /**
14
+ * Read the currently-registered config. Returns an empty object when
15
+ * the user never called `configureTracer` — the fixture treats that
16
+ * as "NDJSON-only, no bindings."
17
+ */
18
+ export declare function getTracerConfig(): HealTracerConfig;
19
+ /**
20
+ * Register a function to run when the current test tears down. Runs
21
+ * BEFORE user bindings' `stop()` so SDKs that use
22
+ * `onTestTeardown(...)` still see any globals the bindings installed.
23
+ *
24
+ * Errors raised by a hook are logged to stderr and swallowed.
25
+ */
26
+ export declare function onTestTeardown(fn: () => void | Promise<void>): void;
27
+ /**
28
+ * Internal. Clears the teardown-hook registry. The fixture calls this
29
+ * at the start of every test to defend against a hook leaking across
30
+ * test boundaries if a prior test crashed before drain ran.
31
+ */
32
+ export declare function resetTeardownHooks(): void;
33
+ /**
34
+ * Internal. Runs every registered teardown hook in registration order,
35
+ * then clears the registry. Errors are logged and swallowed.
36
+ */
37
+ export declare function drainTeardownHooks(): Promise<void>;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright: (c) Myia SAS 2026.
4
+ * This file and its contents are licensed under the AGPLv3 License.
5
+ * Please see the LICENSE file at the root of this repository
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.configureTracer = configureTracer;
9
+ exports.getTracerConfig = getTracerConfig;
10
+ exports.onTestTeardown = onTestTeardown;
11
+ exports.resetTeardownHooks = resetTeardownHooks;
12
+ exports.drainTeardownHooks = drainTeardownHooks;
13
+ let currentConfig = {};
14
+ let teardownHooks = [];
15
+ /**
16
+ * Register the tracer's extension config. Typically called once at
17
+ * the top of `playwright.config.ts`, before `defineConfig(...)`. Later
18
+ * calls overwrite earlier ones — there is no merge.
19
+ */
20
+ function configureTracer(config) {
21
+ currentConfig = config;
22
+ }
23
+ /**
24
+ * Read the currently-registered config. Returns an empty object when
25
+ * the user never called `configureTracer` — the fixture treats that
26
+ * as "NDJSON-only, no bindings."
27
+ */
28
+ function getTracerConfig() {
29
+ return currentConfig;
30
+ }
31
+ /**
32
+ * Register a function to run when the current test tears down. Runs
33
+ * BEFORE user bindings' `stop()` so SDKs that use
34
+ * `onTestTeardown(...)` still see any globals the bindings installed.
35
+ *
36
+ * Errors raised by a hook are logged to stderr and swallowed.
37
+ */
38
+ function onTestTeardown(fn) {
39
+ teardownHooks.push(fn);
40
+ }
41
+ /**
42
+ * Internal. Clears the teardown-hook registry. The fixture calls this
43
+ * at the start of every test to defend against a hook leaking across
44
+ * test boundaries if a prior test crashed before drain ran.
45
+ */
46
+ function resetTeardownHooks() {
47
+ teardownHooks = [];
48
+ }
49
+ /**
50
+ * Internal. Runs every registered teardown hook in registration order,
51
+ * then clears the registry. Errors are logged and swallowed.
52
+ */
53
+ async function drainTeardownHooks() {
54
+ const hooks = teardownHooks;
55
+ teardownHooks = [];
56
+ for (const hook of hooks) {
57
+ try {
58
+ await hook();
59
+ }
60
+ catch (err) {
61
+ console.error('[heal-playwright-tracer] teardown hook failed:', err);
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Copyright: (c) Myia SAS 2026.
3
+ * This file and its contents are licensed under the AGPLv3 License.
4
+ * Please see the LICENSE file at the root of this repository
5
+ */
6
+ import type { TestInfo } from '@playwright/test';
7
+ import type { HealTraceExporter } from '../../domain/trace-event-recorder/port/heal-trace-exporter';
8
+ /**
9
+ * Everything the fixture hands to a exporter or lifecycle factory
10
+ * when a test starts. The `transport` subobject carries the per-test
11
+ * correlation identifiers any outbound exporter needs.
12
+ */
13
+ export interface HealTracerTestContext {
14
+ testInfo: TestInfo;
15
+ /**
16
+ * Absolute path to the per-test `heal-data` directory. Created by
17
+ * the fixture before any factory runs.
18
+ */
19
+ healDataDir: string;
20
+ transport: {
21
+ /**
22
+ * Playwright's `testInfo.testId` — stable hash of
23
+ * (file, title, project). Shared across attempts of the same
24
+ * test, unique per distinct test. Together with `attempt` it
25
+ * forms the per-test-attempt correlation key.
26
+ */
27
+ testId: string;
28
+ attempt: number;
29
+ /** Absolute `testInfo.outputDir`. */
30
+ rootDir: string;
31
+ };
32
+ }
33
+ /**
34
+ * Called once per test. Returns the exporter for that test; the fixture
35
+ * closes it at teardown via `HealTraceExporter.close()`.
36
+ */
37
+ export type HealTraceExporterFactory = (ctx: HealTracerTestContext) => HealTraceExporter;
38
+ /**
39
+ * Per-test setup/teardown pair. Use this to install per-test globals,
40
+ * open telemetry sessions, patch prototypes you'll unpatch later, etc.
41
+ *
42
+ * `setup` receives the `HealTracerTestContext` for the current test.
43
+ * `teardown` takes no arguments — close over any state you need via
44
+ * the enclosing factory or class fields.
45
+ *
46
+ * Errors in `setup` mark that lifecycle as uninstalled — its
47
+ * `teardown` will NOT run. Errors in `teardown` are logged and
48
+ * swallowed so they cannot mask a real test failure.
49
+ */
50
+ export interface HealTestLifecycle {
51
+ setup(ctx: HealTracerTestContext): void | Promise<void>;
52
+ teardown(): void | Promise<void>;
53
+ }
54
+ /**
55
+ * Factory for a `HealTestLifecycle`. Called once per test, before
56
+ * `setup`. Always a factory — not a singleton object — so closure
57
+ * state declared inside the factory is isolated between tests.
58
+ *
59
+ * The factory takes no arguments; the `HealTracerTestContext` arrives
60
+ * via `setup(ctx)` instead. One-place-for-ctx keeps the signature
61
+ * minimal and avoids the "which ctx do I use?" confusion that a
62
+ * two-injection design would create.
63
+ */
64
+ export type HealTestLifecycleFactory = () => HealTestLifecycle;
65
+ /**
66
+ * Shape of the object passed to `configureTracer(...)`. Both fields
67
+ * are optional — an empty config yields the default behaviour
68
+ * (NDJSON-only output, no lifecycles).
69
+ */
70
+ export interface HealTracerConfig {
71
+ exporters?: HealTraceExporterFactory[];
72
+ lifecycles?: HealTestLifecycleFactory[];
73
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright: (c) Myia SAS 2026.
4
+ * This file and its contents are licensed under the AGPLv3 License.
5
+ * Please see the LICENSE file at the root of this repository
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copyright: (c) Myia SAS 2026.
3
+ * This file and its contents are licensed under the AGPLv3 License.
4
+ * Please see the LICENSE file at the root of this repository
5
+ */
6
+ import { expect as rawExpect } from '@playwright/test';
7
+ import { reset } from '../trace-event-recorder-runtime';
8
+ declare const expect: typeof rawExpect;
9
+ type TraceFixtures = {
10
+ _traceAuto: void;
11
+ };
12
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & TraceFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
13
+ export { expect };
14
+ export { reset };