test-proxy-recorder 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -21,6 +21,7 @@ HTTP proxy server for recording and replaying network requests in testing. Works
21
21
  - [Complete Setup Guide](#complete-setup-guide)
22
22
  - [CLI Usage](#cli-usage)
23
23
  - [Playwright Integration](#playwright-integration)
24
+ - [Next.js Integration](#nextjs-integration)
24
25
  - [Control Endpoint](#control-endpoint)
25
26
  - [Typical Workflow](#typical-workflow)
26
27
  - [Recording Format](#recording-format)
@@ -128,14 +129,10 @@ Create `e2e/example.spec.ts`:
128
129
  import { test, expect } from '@playwright/test';
129
130
  import { playwrightProxy } from 'test-proxy-recorder';
130
131
 
131
- // Setup afterEach hook to reset proxy after each test
132
- test.afterEach(async ({ page: _page }, testInfo) => {
133
- await playwrightProxy.after(testInfo);
134
- });
135
-
136
132
  test('example test with proxy', async ({ page }, testInfo) => {
137
133
  // Set proxy mode: 'record' to capture, 'replay' to use recordings
138
- await playwrightProxy.before(testInfo, 'replay');
134
+ // This automatically sets up page.on('close') for cleanup
135
+ await playwrightProxy.before(page, testInfo, 'replay');
139
136
 
140
137
  await page.goto('/');
141
138
  await expect(page.getByText('Welcome')).toBeVisible();
@@ -147,13 +144,13 @@ test('example test with proxy', async ({ page }, testInfo) => {
147
144
  **First run (record mode)**:
148
145
 
149
146
  ```typescript
150
- await playwrightProxy.before(testInfo, 'record');
147
+ await playwrightProxy.before(page, testInfo, 'record');
151
148
  ```
152
149
 
153
150
  **Subsequent runs (replay mode)**:
154
151
 
155
152
  ```typescript
156
- await playwrightProxy.before(testInfo, 'replay');
153
+ await playwrightProxy.before(page, testInfo, 'replay');
157
154
  ```
158
155
 
159
156
  ## CLI Usage
@@ -186,24 +183,26 @@ test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
186
183
 
187
184
  ## Playwright Integration
188
185
 
186
+ ### Session Identification
187
+
188
+ The proxy uses a **custom HTTP header** (`x-test-rcrd-id`) to identify recording sessions. This header is automatically set by the `playwrightProxy.before()` method and works seamlessly with Next.js and other server-side rendering frameworks.
189
+
190
+ **Cookie fallback**: For backward compatibility, the proxy also supports cookie-based session identification, but the custom header is preferred.
191
+
189
192
  ### Basic Test Structure
190
193
 
191
- Every test file using the proxy should follow this pattern:
194
+ Every test using the proxy should follow this pattern:
192
195
 
193
196
  ```typescript
194
197
  import { test } from '@playwright/test';
195
198
  import { playwrightProxy } from 'test-proxy-recorder';
196
199
 
197
- // Setup afterEach hook once per test file
198
- test.afterEach(async ({ page: _page }, testInfo) => {
199
- await playwrightProxy.after(testInfo);
200
- });
201
-
202
200
  test('test name', async ({ page }, testInfo) => {
203
- // 1. Set mode BEFORE test actions
204
- await playwrightProxy.before(testInfo, 'replay');
201
+ // Set mode BEFORE test actions
202
+ // This automatically sets the recording ID header and cleanup handler
203
+ await playwrightProxy.before(page, testInfo, 'replay');
205
204
 
206
- // 2. Test code
205
+ // Test code
207
206
  await page.goto('/page');
208
207
  // Test assertions...
209
208
  });
@@ -215,14 +214,9 @@ test('test name', async ({ page }, testInfo) => {
215
214
  import { test } from '@playwright/test';
216
215
  import { playwrightProxy } from 'test-proxy-recorder';
217
216
 
218
- // Setup afterEach hook to automatically cleanup after each test
219
- test.afterEach(async ({ page: _page }, testInfo) => {
220
- await playwrightProxy.after(testInfo);
221
- });
222
-
223
217
  // Recording mode - captures API responses
224
218
  test('create user', async ({ page }, testInfo) => {
225
- await playwrightProxy.before(testInfo, 'record');
219
+ await playwrightProxy.before(page, testInfo, 'record');
226
220
 
227
221
  await page.goto('/users/new');
228
222
  await page.fill('[name="username"]', 'testuser');
@@ -231,7 +225,7 @@ test('create user', async ({ page }, testInfo) => {
231
225
 
232
226
  // Replay mode - uses recorded responses
233
227
  test('create user', async ({ page }, testInfo) => {
234
- await playwrightProxy.before(testInfo, 'replay');
228
+ await playwrightProxy.before(page, testInfo, 'replay');
235
229
 
236
230
  await page.goto('/users/new');
237
231
  await page.fill('[name="username"]', 'testuser');
@@ -274,6 +268,78 @@ export default defineConfig({
274
268
  });
275
269
  ```
276
270
 
271
+ ## Next.js Integration
272
+
273
+ When testing Next.js applications with server-side rendering (SSR) or API routes, you need to ensure the recording ID header is forwarded to the proxy. The package provides helpers for this.
274
+
275
+ ### Option 1: Using Next.js Middleware (Recommended)
276
+
277
+ Create or update `middleware.ts` in your Next.js project root:
278
+
279
+ ```typescript
280
+ import { NextResponse } from 'next/server';
281
+ import type { NextRequest } from 'next/server';
282
+ import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
283
+
284
+ export function middleware(request: NextRequest) {
285
+ const response = NextResponse.next();
286
+
287
+ // Forward the recording ID header during tests
288
+ // Only runs in non-production or when TEST_PROXY_RECORDER_ENABLED=true
289
+ setNextProxyHeaders(request, response);
290
+
291
+ return response;
292
+ }
293
+ ```
294
+
295
+ **Environment Variables:**
296
+ - Automatically skipped when `NODE_ENV=production`
297
+ - Can be explicitly enabled in production with `TEST_PROXY_RECORDER_ENABLED=true`
298
+
299
+ ### Option 2: Manual Header Forwarding in API Routes
300
+
301
+ For API routes or server components, manually include the header in fetch requests:
302
+
303
+ ```typescript
304
+ // app/api/data/route.ts
305
+ import { headers } from 'next/headers';
306
+ import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
307
+
308
+ export async function GET() {
309
+ const requestHeaders = await headers();
310
+
311
+ const response = await fetch('http://localhost:8100/api/data', {
312
+ headers: createHeadersWithRecordingId(requestHeaders, {
313
+ 'Content-Type': 'application/json',
314
+ })
315
+ });
316
+
317
+ return Response.json(await response.json());
318
+ }
319
+ ```
320
+
321
+ ### Option 3: Using getRecordingId Helper
322
+
323
+ For more control, extract the recording ID and use it manually:
324
+
325
+ ```typescript
326
+ import { headers } from 'next/headers';
327
+ import { getRecordingId, RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
328
+
329
+ export async function GET() {
330
+ const recordingId = getRecordingId(await headers());
331
+
332
+ const response = await fetch('http://localhost:8100/api/data', {
333
+ headers: {
334
+ 'Content-Type': 'application/json',
335
+ ...(recordingId && { [RECORDING_ID_HEADER]: recordingId })
336
+ }
337
+ });
338
+
339
+ return Response.json(await response.json());
340
+ }
341
+ ```
342
+
277
343
  ## Control Endpoint
278
344
 
279
345
  The proxy exposes a control endpoint at `/__control` for programmatic mode switching.
@@ -410,20 +476,20 @@ class ProxyServer {
410
476
  ### Playwright Integration
411
477
 
412
478
  ```typescript
413
- import { playwrightProxy, setProxyMode } from 'test-proxy-recorder';
479
+ import { playwrightProxy, setProxyMode, RECORDING_ID_HEADER } from 'test-proxy-recorder';
480
+ import type { Page } from '@playwright/test';
414
481
 
415
482
  // Main helper for Playwright tests
416
483
  const playwrightProxy = {
417
- // Set proxy mode before test
484
+ // Set proxy mode before test and configure page with recording ID header
485
+ // Automatically sets up page.on('close') handler for cleanup
418
486
  async before(
487
+ page: Page,
419
488
  testInfo: TestInfo,
420
489
  mode: 'record' | 'replay' | 'transparent',
421
490
  timeout?: number
422
491
  ): Promise<void>;
423
492
 
424
- // Reset replay session and return to transparent mode after test
425
- // Resets sequence counters to ensure next replay starts fresh
426
- async after(testInfo: TestInfo): Promise<void>;
427
493
 
428
494
  // Global teardown - switches proxy to transparent mode
429
495
  // Use in Playwright's globalTeardown configuration
@@ -436,6 +502,41 @@ async function setProxyMode(
436
502
  id?: string,
437
503
  timeout?: number
438
504
  ): Promise<void>;
505
+
506
+ // Recording ID header constant
507
+ const RECORDING_ID_HEADER: string; // 'x-test-rcrd-id'
508
+ ```
509
+
510
+ ### Next.js Integration
511
+
512
+ **IMPORTANT**: Use the `/nextjs` import path to avoid webpack bundling issues in Next.js:
513
+
514
+ ```typescript
515
+ import {
516
+ setNextProxyHeaders,
517
+ getRecordingId,
518
+ createHeadersWithRecordingId,
519
+ RECORDING_ID_HEADER
520
+ } from 'test-proxy-recorder/nextjs';
521
+ import type { NextRequest, NextResponse } from 'next/server';
522
+
523
+ // Forward recording ID header in Next.js middleware
524
+ // Automatically skipped in production unless TEST_PROXY_RECORDER_ENABLED=true
525
+ function setNextProxyHeaders(
526
+ request: NextRequest,
527
+ response: NextResponse
528
+ ): void;
529
+
530
+ // Get recording ID from request headers
531
+ function getRecordingId(
532
+ requestHeaders: NextRequest | Headers
533
+ ): string | null;
534
+
535
+ // Create headers object with recording ID for fetch requests
536
+ function createHeadersWithRecordingId(
537
+ requestHeaders: NextRequest | Headers,
538
+ additionalHeaders?: Record<string, string>
539
+ ): Record<string, string>;
439
540
  ```
440
541
 
441
542
  ### Control Endpoint
@@ -1,4 +1,4 @@
1
- import { TestInfo } from '@playwright/test';
1
+ import { TestInfo, Page } from '@playwright/test';
2
2
  import http from 'node:http';
3
3
 
4
4
  declare const Modes: {
@@ -82,22 +82,18 @@ declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
82
82
  declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
83
83
  /**
84
84
  * Playwright test fixture helper for managing proxy mode
85
- * Use this in beforeEach/afterEach hooks
85
+ * Use this in test functions with page.on('close') for automatic cleanup
86
86
  */
87
87
  declare const playwrightProxy: {
88
88
  /**
89
- * Setup before test - sets the proxy mode
89
+ * Setup before test - sets the proxy mode and configures page with custom header
90
+ * Automatically sets up page.on('close') handler for cleanup
91
+ * @param page - Playwright page object
90
92
  * @param testInfo - Playwright test info object
91
93
  * @param mode - The proxy mode to use for this test
92
94
  * @param timeout - Optional timeout in milliseconds
93
95
  */
94
- before(testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
95
- /**
96
- * Cleanup after test - resets replay session by re-entering replay mode
97
- * switchToReplayMode automatically clears sequence counters
98
- * @param testInfo - Playwright test info object
99
- */
100
- after(testInfo: PlaywrightTestInfo): Promise<void>;
96
+ before(page: Page, testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
101
97
  /**
102
98
  * Global teardown - switches proxy to transparent mode
103
99
  * Use this in Playwright's globalTeardown to ensure clean state
@@ -1,4 +1,4 @@
1
- import { TestInfo } from '@playwright/test';
1
+ import { TestInfo, Page } from '@playwright/test';
2
2
  import http from 'node:http';
3
3
 
4
4
  declare const Modes: {
@@ -82,22 +82,18 @@ declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
82
82
  declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
83
83
  /**
84
84
  * Playwright test fixture helper for managing proxy mode
85
- * Use this in beforeEach/afterEach hooks
85
+ * Use this in test functions with page.on('close') for automatic cleanup
86
86
  */
87
87
  declare const playwrightProxy: {
88
88
  /**
89
- * Setup before test - sets the proxy mode
89
+ * Setup before test - sets the proxy mode and configures page with custom header
90
+ * Automatically sets up page.on('close') handler for cleanup
91
+ * @param page - Playwright page object
90
92
  * @param testInfo - Playwright test info object
91
93
  * @param mode - The proxy mode to use for this test
92
94
  * @param timeout - Optional timeout in milliseconds
93
95
  */
94
- before(testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
95
- /**
96
- * Cleanup after test - resets replay session by re-entering replay mode
97
- * switchToReplayMode automatically clears sequence counters
98
- * @param testInfo - Playwright test info object
99
- */
100
- after(testInfo: PlaywrightTestInfo): Promise<void>;
96
+ before(page: Page, testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
101
97
  /**
102
98
  * Global teardown - switches proxy to transparent mode
103
99
  * Use this in Playwright's globalTeardown to ensure clean state
package/dist/index.cjs CHANGED
@@ -19,8 +19,6 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
19
19
  var path__default = /*#__PURE__*/_interopDefault(path);
20
20
  var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
21
21
 
22
- // src/ProxyServer.ts
23
-
24
22
  // src/constants.ts
25
23
  var DEFAULT_TIMEOUT_MS = 120 * 1e3;
26
24
  var HTTP_STATUS_BAD_GATEWAY = 502;
@@ -28,6 +26,7 @@ var HTTP_STATUS_OK = 200;
28
26
  var HTTP_STATUS_BAD_REQUEST = 400;
29
27
  var HTTP_STATUS_NOT_FOUND = 404;
30
28
  var CONTROL_ENDPOINT = "/__control";
29
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
31
30
 
32
31
  // src/types.ts
33
32
  var Modes = {
@@ -200,7 +199,7 @@ var ProxyServer = class {
200
199
  return {
201
200
  "access-control-allow-origin": origin || "*",
202
201
  "access-control-allow-credentials": "true",
203
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
202
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
204
203
  "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
205
204
  "access-control-expose-headers": "*"
206
205
  };
@@ -214,9 +213,22 @@ var ProxyServer = class {
214
213
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
215
214
  return target;
216
215
  }
216
+ /**
217
+ * Extract recording ID from custom HTTP header
218
+ * Used for concurrent replay session routing, especially with Next.js
219
+ * @param req The incoming HTTP request
220
+ * @returns The recording ID from header, or null if not found
221
+ */
222
+ getRecordingIdFromHeader(req) {
223
+ const headerValue = req.headers[RECORDING_ID_HEADER];
224
+ if (!headerValue) {
225
+ return null;
226
+ }
227
+ return Array.isArray(headerValue) ? headerValue[0] : headerValue;
228
+ }
217
229
  /**
218
230
  * Extract recording ID from request cookie
219
- * Used for concurrent replay session routing
231
+ * Used for concurrent replay session routing (fallback method)
220
232
  * @param req The incoming HTTP request
221
233
  * @returns The recording ID from cookie, or null if not found
222
234
  */
@@ -228,6 +240,14 @@ var ProxyServer = class {
228
240
  const match = cookies.match(/proxy-recording-id=([^;]+)/);
229
241
  return match ? decodeURIComponent(match[1]) : null;
230
242
  }
243
+ /**
244
+ * Extract recording ID from request using custom header (preferred) or cookie (fallback)
245
+ * @param req The incoming HTTP request
246
+ * @returns The recording ID, or null if not found
247
+ */
248
+ getRecordingIdFromRequest(req) {
249
+ return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
250
+ }
231
251
  /**
232
252
  * Get or create a replay session state for a given recording ID
233
253
  * @param recordingId The recording ID to get/create session for
@@ -505,7 +525,7 @@ var ProxyServer = class {
505
525
  return true;
506
526
  }
507
527
  async handleReplayRequest(req, res) {
508
- const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
528
+ const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
509
529
  if (!recordingId) {
510
530
  const corsHeaders = this.getCorsHeaders(req);
511
531
  res.writeHead(HTTP_STATUS_BAD_REQUEST, {
@@ -954,23 +974,29 @@ async function stopProxy(testInfo) {
954
974
  }
955
975
  var playwrightProxy = {
956
976
  /**
957
- * Setup before test - sets the proxy mode
977
+ * Setup before test - sets the proxy mode and configures page with custom header
978
+ * Automatically sets up page.on('close') handler for cleanup
979
+ * @param page - Playwright page object
958
980
  * @param testInfo - Playwright test info object
959
981
  * @param mode - The proxy mode to use for this test
960
982
  * @param timeout - Optional timeout in milliseconds
961
983
  */
962
- async before(testInfo, mode, timeout) {
984
+ async before(page, testInfo, mode, timeout) {
963
985
  const sessionId = generateSessionId(testInfo);
986
+ await page.setExtraHTTPHeaders({
987
+ [RECORDING_ID_HEADER]: sessionId
988
+ });
964
989
  await setProxyMode(mode, sessionId, timeout);
965
- },
966
- /**
967
- * Cleanup after test - resets replay session by re-entering replay mode
968
- * switchToReplayMode automatically clears sequence counters
969
- * @param testInfo - Playwright test info object
970
- */
971
- async after(testInfo) {
972
- const sessionId = generateSessionId(testInfo);
973
- await setProxyMode(Modes.replay, sessionId);
990
+ page.on("close", async () => {
991
+ try {
992
+ await setProxyMode(Modes.replay, sessionId);
993
+ console.log(
994
+ `[Cleanup] Switched to transparent mode for session: ${sessionId}`
995
+ );
996
+ } catch (error) {
997
+ console.error("[Cleanup] Error during page close cleanup:", error);
998
+ }
999
+ });
974
1000
  },
975
1001
  /**
976
1002
  * Global teardown - switches proxy to transparent mode
@@ -981,9 +1007,45 @@ var playwrightProxy = {
981
1007
  }
982
1008
  };
983
1009
 
1010
+ // src/nextjs/middleware.ts
1011
+ function isRecorderEnabled() {
1012
+ const isProduction = process.env.NODE_ENV === "production";
1013
+ const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
1014
+ return !isProduction || isExplicitlyEnabled;
1015
+ }
1016
+ function setNextProxyHeaders(request, response) {
1017
+ if (!isRecorderEnabled()) {
1018
+ return;
1019
+ }
1020
+ const recordingId = request.headers.get(RECORDING_ID_HEADER);
1021
+ if (recordingId) {
1022
+ response.headers.set(RECORDING_ID_HEADER, recordingId);
1023
+ }
1024
+ }
1025
+ function getRecordingId(requestHeaders) {
1026
+ if (requestHeaders instanceof Headers) {
1027
+ return requestHeaders.get(RECORDING_ID_HEADER);
1028
+ }
1029
+ return requestHeaders.headers.get(RECORDING_ID_HEADER);
1030
+ }
1031
+ function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
1032
+ if (!isRecorderEnabled()) {
1033
+ return additionalHeaders;
1034
+ }
1035
+ const recordingId = getRecordingId(requestHeaders);
1036
+ return {
1037
+ ...additionalHeaders,
1038
+ ...recordingId && { [RECORDING_ID_HEADER]: recordingId }
1039
+ };
1040
+ }
1041
+
984
1042
  exports.ProxyServer = ProxyServer;
1043
+ exports.RECORDING_ID_HEADER = RECORDING_ID_HEADER;
1044
+ exports.createHeadersWithRecordingId = createHeadersWithRecordingId;
985
1045
  exports.generateSessionId = generateSessionId;
1046
+ exports.getRecordingId = getRecordingId;
986
1047
  exports.playwrightProxy = playwrightProxy;
1048
+ exports.setNextProxyHeaders = setNextProxyHeaders;
987
1049
  exports.setProxyMode = setProxyMode;
988
1050
  exports.startRecording = startRecording;
989
1051
  exports.startReplay = startReplay;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
+ export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.cjs';
1
2
  import http from 'node:http';
2
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-nmNRt1WE.cjs';
3
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CVuiglPk.cjs';
3
4
  import '@playwright/test';
4
5
 
5
6
  declare class ProxyServer {
@@ -28,13 +29,26 @@ declare class ProxyServer {
28
29
  private getCorsHeaders;
29
30
  private addCorsHeaders;
30
31
  private getTarget;
32
+ /**
33
+ * Extract recording ID from custom HTTP header
34
+ * Used for concurrent replay session routing, especially with Next.js
35
+ * @param req The incoming HTTP request
36
+ * @returns The recording ID from header, or null if not found
37
+ */
38
+ private getRecordingIdFromHeader;
31
39
  /**
32
40
  * Extract recording ID from request cookie
33
- * Used for concurrent replay session routing
41
+ * Used for concurrent replay session routing (fallback method)
34
42
  * @param req The incoming HTTP request
35
43
  * @returns The recording ID from cookie, or null if not found
36
44
  */
37
45
  private getRecordingIdFromCookie;
46
+ /**
47
+ * Extract recording ID from request using custom header (preferred) or cookie (fallback)
48
+ * @param req The incoming HTTP request
49
+ * @returns The recording ID, or null if not found
50
+ */
51
+ private getRecordingIdFromRequest;
38
52
  /**
39
53
  * Get or create a replay session state for a given recording ID
40
54
  * @param recordingId The recording ID to get/create session for
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.js';
1
2
  import http from 'node:http';
2
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-nmNRt1WE.js';
3
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CVuiglPk.js';
3
4
  import '@playwright/test';
4
5
 
5
6
  declare class ProxyServer {
@@ -28,13 +29,26 @@ declare class ProxyServer {
28
29
  private getCorsHeaders;
29
30
  private addCorsHeaders;
30
31
  private getTarget;
32
+ /**
33
+ * Extract recording ID from custom HTTP header
34
+ * Used for concurrent replay session routing, especially with Next.js
35
+ * @param req The incoming HTTP request
36
+ * @returns The recording ID from header, or null if not found
37
+ */
38
+ private getRecordingIdFromHeader;
31
39
  /**
32
40
  * Extract recording ID from request cookie
33
- * Used for concurrent replay session routing
41
+ * Used for concurrent replay session routing (fallback method)
34
42
  * @param req The incoming HTTP request
35
43
  * @returns The recording ID from cookie, or null if not found
36
44
  */
37
45
  private getRecordingIdFromCookie;
46
+ /**
47
+ * Extract recording ID from request using custom header (preferred) or cookie (fallback)
48
+ * @param req The incoming HTTP request
49
+ * @returns The recording ID, or null if not found
50
+ */
51
+ private getRecordingIdFromRequest;
38
52
  /**
39
53
  * Get or create a replay session state for a given recording ID
40
54
  * @param recordingId The recording ID to get/create session for
package/dist/index.mjs CHANGED
@@ -7,8 +7,6 @@ import crypto from 'crypto';
7
7
  import path from 'path';
8
8
  import filenamify2 from 'filenamify';
9
9
 
10
- // src/ProxyServer.ts
11
-
12
10
  // src/constants.ts
13
11
  var DEFAULT_TIMEOUT_MS = 120 * 1e3;
14
12
  var HTTP_STATUS_BAD_GATEWAY = 502;
@@ -16,6 +14,7 @@ var HTTP_STATUS_OK = 200;
16
14
  var HTTP_STATUS_BAD_REQUEST = 400;
17
15
  var HTTP_STATUS_NOT_FOUND = 404;
18
16
  var CONTROL_ENDPOINT = "/__control";
17
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
19
18
 
20
19
  // src/types.ts
21
20
  var Modes = {
@@ -188,7 +187,7 @@ var ProxyServer = class {
188
187
  return {
189
188
  "access-control-allow-origin": origin || "*",
190
189
  "access-control-allow-credentials": "true",
191
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
190
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
192
191
  "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
193
192
  "access-control-expose-headers": "*"
194
193
  };
@@ -202,9 +201,22 @@ var ProxyServer = class {
202
201
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
203
202
  return target;
204
203
  }
204
+ /**
205
+ * Extract recording ID from custom HTTP header
206
+ * Used for concurrent replay session routing, especially with Next.js
207
+ * @param req The incoming HTTP request
208
+ * @returns The recording ID from header, or null if not found
209
+ */
210
+ getRecordingIdFromHeader(req) {
211
+ const headerValue = req.headers[RECORDING_ID_HEADER];
212
+ if (!headerValue) {
213
+ return null;
214
+ }
215
+ return Array.isArray(headerValue) ? headerValue[0] : headerValue;
216
+ }
205
217
  /**
206
218
  * Extract recording ID from request cookie
207
- * Used for concurrent replay session routing
219
+ * Used for concurrent replay session routing (fallback method)
208
220
  * @param req The incoming HTTP request
209
221
  * @returns The recording ID from cookie, or null if not found
210
222
  */
@@ -216,6 +228,14 @@ var ProxyServer = class {
216
228
  const match = cookies.match(/proxy-recording-id=([^;]+)/);
217
229
  return match ? decodeURIComponent(match[1]) : null;
218
230
  }
231
+ /**
232
+ * Extract recording ID from request using custom header (preferred) or cookie (fallback)
233
+ * @param req The incoming HTTP request
234
+ * @returns The recording ID, or null if not found
235
+ */
236
+ getRecordingIdFromRequest(req) {
237
+ return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
238
+ }
219
239
  /**
220
240
  * Get or create a replay session state for a given recording ID
221
241
  * @param recordingId The recording ID to get/create session for
@@ -493,7 +513,7 @@ var ProxyServer = class {
493
513
  return true;
494
514
  }
495
515
  async handleReplayRequest(req, res) {
496
- const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
516
+ const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
497
517
  if (!recordingId) {
498
518
  const corsHeaders = this.getCorsHeaders(req);
499
519
  res.writeHead(HTTP_STATUS_BAD_REQUEST, {
@@ -942,23 +962,29 @@ async function stopProxy(testInfo) {
942
962
  }
943
963
  var playwrightProxy = {
944
964
  /**
945
- * Setup before test - sets the proxy mode
965
+ * Setup before test - sets the proxy mode and configures page with custom header
966
+ * Automatically sets up page.on('close') handler for cleanup
967
+ * @param page - Playwright page object
946
968
  * @param testInfo - Playwright test info object
947
969
  * @param mode - The proxy mode to use for this test
948
970
  * @param timeout - Optional timeout in milliseconds
949
971
  */
950
- async before(testInfo, mode, timeout) {
972
+ async before(page, testInfo, mode, timeout) {
951
973
  const sessionId = generateSessionId(testInfo);
974
+ await page.setExtraHTTPHeaders({
975
+ [RECORDING_ID_HEADER]: sessionId
976
+ });
952
977
  await setProxyMode(mode, sessionId, timeout);
953
- },
954
- /**
955
- * Cleanup after test - resets replay session by re-entering replay mode
956
- * switchToReplayMode automatically clears sequence counters
957
- * @param testInfo - Playwright test info object
958
- */
959
- async after(testInfo) {
960
- const sessionId = generateSessionId(testInfo);
961
- await setProxyMode(Modes.replay, sessionId);
978
+ page.on("close", async () => {
979
+ try {
980
+ await setProxyMode(Modes.replay, sessionId);
981
+ console.log(
982
+ `[Cleanup] Switched to transparent mode for session: ${sessionId}`
983
+ );
984
+ } catch (error) {
985
+ console.error("[Cleanup] Error during page close cleanup:", error);
986
+ }
987
+ });
962
988
  },
963
989
  /**
964
990
  * Global teardown - switches proxy to transparent mode
@@ -969,6 +995,38 @@ var playwrightProxy = {
969
995
  }
970
996
  };
971
997
 
972
- export { ProxyServer, generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
998
+ // src/nextjs/middleware.ts
999
+ function isRecorderEnabled() {
1000
+ const isProduction = process.env.NODE_ENV === "production";
1001
+ const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
1002
+ return !isProduction || isExplicitlyEnabled;
1003
+ }
1004
+ function setNextProxyHeaders(request, response) {
1005
+ if (!isRecorderEnabled()) {
1006
+ return;
1007
+ }
1008
+ const recordingId = request.headers.get(RECORDING_ID_HEADER);
1009
+ if (recordingId) {
1010
+ response.headers.set(RECORDING_ID_HEADER, recordingId);
1011
+ }
1012
+ }
1013
+ function getRecordingId(requestHeaders) {
1014
+ if (requestHeaders instanceof Headers) {
1015
+ return requestHeaders.get(RECORDING_ID_HEADER);
1016
+ }
1017
+ return requestHeaders.headers.get(RECORDING_ID_HEADER);
1018
+ }
1019
+ function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
1020
+ if (!isRecorderEnabled()) {
1021
+ return additionalHeaders;
1022
+ }
1023
+ const recordingId = getRecordingId(requestHeaders);
1024
+ return {
1025
+ ...additionalHeaders,
1026
+ ...recordingId && { [RECORDING_ID_HEADER]: recordingId }
1027
+ };
1028
+ }
1029
+
1030
+ export { ProxyServer, RECORDING_ID_HEADER, createHeadersWithRecordingId, generateSessionId, getRecordingId, playwrightProxy, setNextProxyHeaders, setProxyMode, startRecording, startReplay, stopProxy };
973
1031
  //# sourceMappingURL=index.mjs.map
974
1032
  //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
5
+
6
+ // src/nextjs/middleware.ts
7
+ function isRecorderEnabled() {
8
+ const isProduction = process.env.NODE_ENV === "production";
9
+ const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
10
+ return !isProduction || isExplicitlyEnabled;
11
+ }
12
+ function setNextProxyHeaders(request, response) {
13
+ if (!isRecorderEnabled()) {
14
+ return;
15
+ }
16
+ const recordingId = request.headers.get(RECORDING_ID_HEADER);
17
+ if (recordingId) {
18
+ response.headers.set(RECORDING_ID_HEADER, recordingId);
19
+ }
20
+ }
21
+ function getRecordingId(requestHeaders) {
22
+ if (requestHeaders instanceof Headers) {
23
+ return requestHeaders.get(RECORDING_ID_HEADER);
24
+ }
25
+ return requestHeaders.headers.get(RECORDING_ID_HEADER);
26
+ }
27
+ function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
28
+ if (!isRecorderEnabled()) {
29
+ return additionalHeaders;
30
+ }
31
+ const recordingId = getRecordingId(requestHeaders);
32
+ return {
33
+ ...additionalHeaders,
34
+ ...recordingId && { [RECORDING_ID_HEADER]: recordingId }
35
+ };
36
+ }
37
+
38
+ exports.RECORDING_ID_HEADER = RECORDING_ID_HEADER;
39
+ exports.createHeadersWithRecordingId = createHeadersWithRecordingId;
40
+ exports.getRecordingId = getRecordingId;
41
+ exports.setNextProxyHeaders = setNextProxyHeaders;
42
+ //# sourceMappingURL=index.cjs.map
43
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,89 @@
1
+ declare const RECORDING_ID_HEADER = "x-test-rcrd-id";
2
+
3
+ /**
4
+ * Minimal type for Next.js Request - compatible with next/server's NextRequest
5
+ * We define this locally to avoid requiring Next.js as a dependency
6
+ */
7
+ interface NextJSRequest {
8
+ headers: Headers;
9
+ }
10
+ /**
11
+ * Minimal type for Next.js Response - compatible with next/server's NextResponse
12
+ * We define this locally to avoid requiring Next.js as a dependency
13
+ */
14
+ interface NextJSResponse {
15
+ headers: Headers;
16
+ }
17
+ /**
18
+ * Next.js middleware helper for forwarding test proxy recording headers
19
+ * Automatically forwards the recording ID header from incoming requests to the proxy
20
+ * Only runs in non-production environments or when TEST_PROXY_RECORDER_ENABLED is set
21
+ *
22
+ * @example
23
+ * // middleware.ts
24
+ * import { NextResponse } from 'next/server';
25
+ * import type { NextRequest } from 'next/server';
26
+ * import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
27
+ *
28
+ * export function middleware(request: NextRequest) {
29
+ * const response = NextResponse.next();
30
+ * // Only forwards headers in test/dev environments
31
+ * setNextProxyHeaders(request, response);
32
+ * return response;
33
+ * }
34
+ *
35
+ * @param request - Next.js request object (NextRequest from next/server)
36
+ * @param response - Next.js response object (NextResponse from next/server)
37
+ */
38
+ declare function setNextProxyHeaders(request: NextJSRequest, response: NextJSResponse): void;
39
+ /**
40
+ * Get the recording ID from the request if present
41
+ * Useful for manually adding the header to fetch requests in Next.js
42
+ *
43
+ * @example
44
+ * // In your API route or server component
45
+ * import { getRecordingId } from 'test-proxy-recorder/nextjs';
46
+ * import { headers } from 'next/headers';
47
+ *
48
+ * export async function GET() {
49
+ * const recordingId = getRecordingId(headers());
50
+ *
51
+ * const response = await fetch('http://localhost:8100/api/data', {
52
+ * headers: {
53
+ * ...(recordingId && { 'x-test-rcrd-id': recordingId })
54
+ * }
55
+ * });
56
+ *
57
+ * return Response.json(await response.json());
58
+ * }
59
+ *
60
+ * @param requestHeaders - Next.js headers object or NextRequest from next/server
61
+ * @returns The recording ID if present, null otherwise
62
+ */
63
+ declare function getRecordingId(requestHeaders: NextJSRequest | Headers): string | null;
64
+ /**
65
+ * Create headers object with recording ID for fetch requests
66
+ * Use this helper when making fetch requests in Next.js to forward the recording ID
67
+ *
68
+ * @example
69
+ * // In your API route or server component
70
+ * import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
71
+ * import { headers } from 'next/headers';
72
+ *
73
+ * export async function GET() {
74
+ * const response = await fetch('http://localhost:8100/api/data', {
75
+ * headers: createHeadersWithRecordingId(headers(), {
76
+ * 'Content-Type': 'application/json',
77
+ * })
78
+ * });
79
+ *
80
+ * return Response.json(await response.json());
81
+ * }
82
+ *
83
+ * @param requestHeaders - Next.js headers object or NextRequest from next/server
84
+ * @param additionalHeaders - Optional additional headers to include
85
+ * @returns Headers object with recording ID if present
86
+ */
87
+ declare function createHeadersWithRecordingId(requestHeaders: NextJSRequest | Headers, additionalHeaders?: Record<string, string>): Record<string, string>;
88
+
89
+ export { type NextJSRequest, type NextJSResponse, RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders };
@@ -0,0 +1,89 @@
1
+ declare const RECORDING_ID_HEADER = "x-test-rcrd-id";
2
+
3
+ /**
4
+ * Minimal type for Next.js Request - compatible with next/server's NextRequest
5
+ * We define this locally to avoid requiring Next.js as a dependency
6
+ */
7
+ interface NextJSRequest {
8
+ headers: Headers;
9
+ }
10
+ /**
11
+ * Minimal type for Next.js Response - compatible with next/server's NextResponse
12
+ * We define this locally to avoid requiring Next.js as a dependency
13
+ */
14
+ interface NextJSResponse {
15
+ headers: Headers;
16
+ }
17
+ /**
18
+ * Next.js middleware helper for forwarding test proxy recording headers
19
+ * Automatically forwards the recording ID header from incoming requests to the proxy
20
+ * Only runs in non-production environments or when TEST_PROXY_RECORDER_ENABLED is set
21
+ *
22
+ * @example
23
+ * // middleware.ts
24
+ * import { NextResponse } from 'next/server';
25
+ * import type { NextRequest } from 'next/server';
26
+ * import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
27
+ *
28
+ * export function middleware(request: NextRequest) {
29
+ * const response = NextResponse.next();
30
+ * // Only forwards headers in test/dev environments
31
+ * setNextProxyHeaders(request, response);
32
+ * return response;
33
+ * }
34
+ *
35
+ * @param request - Next.js request object (NextRequest from next/server)
36
+ * @param response - Next.js response object (NextResponse from next/server)
37
+ */
38
+ declare function setNextProxyHeaders(request: NextJSRequest, response: NextJSResponse): void;
39
+ /**
40
+ * Get the recording ID from the request if present
41
+ * Useful for manually adding the header to fetch requests in Next.js
42
+ *
43
+ * @example
44
+ * // In your API route or server component
45
+ * import { getRecordingId } from 'test-proxy-recorder/nextjs';
46
+ * import { headers } from 'next/headers';
47
+ *
48
+ * export async function GET() {
49
+ * const recordingId = getRecordingId(headers());
50
+ *
51
+ * const response = await fetch('http://localhost:8100/api/data', {
52
+ * headers: {
53
+ * ...(recordingId && { 'x-test-rcrd-id': recordingId })
54
+ * }
55
+ * });
56
+ *
57
+ * return Response.json(await response.json());
58
+ * }
59
+ *
60
+ * @param requestHeaders - Next.js headers object or NextRequest from next/server
61
+ * @returns The recording ID if present, null otherwise
62
+ */
63
+ declare function getRecordingId(requestHeaders: NextJSRequest | Headers): string | null;
64
+ /**
65
+ * Create headers object with recording ID for fetch requests
66
+ * Use this helper when making fetch requests in Next.js to forward the recording ID
67
+ *
68
+ * @example
69
+ * // In your API route or server component
70
+ * import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
71
+ * import { headers } from 'next/headers';
72
+ *
73
+ * export async function GET() {
74
+ * const response = await fetch('http://localhost:8100/api/data', {
75
+ * headers: createHeadersWithRecordingId(headers(), {
76
+ * 'Content-Type': 'application/json',
77
+ * })
78
+ * });
79
+ *
80
+ * return Response.json(await response.json());
81
+ * }
82
+ *
83
+ * @param requestHeaders - Next.js headers object or NextRequest from next/server
84
+ * @param additionalHeaders - Optional additional headers to include
85
+ * @returns Headers object with recording ID if present
86
+ */
87
+ declare function createHeadersWithRecordingId(requestHeaders: NextJSRequest | Headers, additionalHeaders?: Record<string, string>): Record<string, string>;
88
+
89
+ export { type NextJSRequest, type NextJSResponse, RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders };
@@ -0,0 +1,38 @@
1
+ // src/constants.ts
2
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
3
+
4
+ // src/nextjs/middleware.ts
5
+ function isRecorderEnabled() {
6
+ const isProduction = process.env.NODE_ENV === "production";
7
+ const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
8
+ return !isProduction || isExplicitlyEnabled;
9
+ }
10
+ function setNextProxyHeaders(request, response) {
11
+ if (!isRecorderEnabled()) {
12
+ return;
13
+ }
14
+ const recordingId = request.headers.get(RECORDING_ID_HEADER);
15
+ if (recordingId) {
16
+ response.headers.set(RECORDING_ID_HEADER, recordingId);
17
+ }
18
+ }
19
+ function getRecordingId(requestHeaders) {
20
+ if (requestHeaders instanceof Headers) {
21
+ return requestHeaders.get(RECORDING_ID_HEADER);
22
+ }
23
+ return requestHeaders.headers.get(RECORDING_ID_HEADER);
24
+ }
25
+ function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
26
+ if (!isRecorderEnabled()) {
27
+ return additionalHeaders;
28
+ }
29
+ const recordingId = getRecordingId(requestHeaders);
30
+ return {
31
+ ...additionalHeaders,
32
+ ...recordingId && { [RECORDING_ID_HEADER]: recordingId }
33
+ };
34
+ }
35
+
36
+ export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders };
37
+ //# sourceMappingURL=index.mjs.map
38
+ //# sourceMappingURL=index.mjs.map
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ // src/constants.ts
4
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
5
+
3
6
  // src/types.ts
4
7
  var Modes = {
5
8
  transparent: "transparent",
@@ -86,23 +89,29 @@ async function stopProxy(testInfo) {
86
89
  }
87
90
  var playwrightProxy = {
88
91
  /**
89
- * Setup before test - sets the proxy mode
92
+ * Setup before test - sets the proxy mode and configures page with custom header
93
+ * Automatically sets up page.on('close') handler for cleanup
94
+ * @param page - Playwright page object
90
95
  * @param testInfo - Playwright test info object
91
96
  * @param mode - The proxy mode to use for this test
92
97
  * @param timeout - Optional timeout in milliseconds
93
98
  */
94
- async before(testInfo, mode, timeout) {
99
+ async before(page, testInfo, mode, timeout) {
95
100
  const sessionId = generateSessionId(testInfo);
101
+ await page.setExtraHTTPHeaders({
102
+ [RECORDING_ID_HEADER]: sessionId
103
+ });
96
104
  await setProxyMode(mode, sessionId, timeout);
97
- },
98
- /**
99
- * Cleanup after test - resets replay session by re-entering replay mode
100
- * switchToReplayMode automatically clears sequence counters
101
- * @param testInfo - Playwright test info object
102
- */
103
- async after(testInfo) {
104
- const sessionId = generateSessionId(testInfo);
105
- await setProxyMode(Modes.replay, sessionId);
105
+ page.on("close", async () => {
106
+ try {
107
+ await setProxyMode(Modes.replay, sessionId);
108
+ console.log(
109
+ `[Cleanup] Switched to transparent mode for session: ${sessionId}`
110
+ );
111
+ } catch (error) {
112
+ console.error("[Cleanup] Error during page close cleanup:", error);
113
+ }
114
+ });
106
115
  },
107
116
  /**
108
117
  * Global teardown - switches proxy to transparent mode
@@ -1,3 +1,3 @@
1
1
  import '@playwright/test';
2
- export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-nmNRt1WE.cjs';
2
+ export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-CVuiglPk.cjs';
3
3
  import 'node:http';
@@ -1,3 +1,3 @@
1
1
  import '@playwright/test';
2
- export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-nmNRt1WE.js';
2
+ export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-CVuiglPk.js';
3
3
  import 'node:http';
@@ -1,3 +1,6 @@
1
+ // src/constants.ts
2
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
3
+
1
4
  // src/types.ts
2
5
  var Modes = {
3
6
  transparent: "transparent",
@@ -84,23 +87,29 @@ async function stopProxy(testInfo) {
84
87
  }
85
88
  var playwrightProxy = {
86
89
  /**
87
- * Setup before test - sets the proxy mode
90
+ * Setup before test - sets the proxy mode and configures page with custom header
91
+ * Automatically sets up page.on('close') handler for cleanup
92
+ * @param page - Playwright page object
88
93
  * @param testInfo - Playwright test info object
89
94
  * @param mode - The proxy mode to use for this test
90
95
  * @param timeout - Optional timeout in milliseconds
91
96
  */
92
- async before(testInfo, mode, timeout) {
97
+ async before(page, testInfo, mode, timeout) {
93
98
  const sessionId = generateSessionId(testInfo);
99
+ await page.setExtraHTTPHeaders({
100
+ [RECORDING_ID_HEADER]: sessionId
101
+ });
94
102
  await setProxyMode(mode, sessionId, timeout);
95
- },
96
- /**
97
- * Cleanup after test - resets replay session by re-entering replay mode
98
- * switchToReplayMode automatically clears sequence counters
99
- * @param testInfo - Playwright test info object
100
- */
101
- async after(testInfo) {
102
- const sessionId = generateSessionId(testInfo);
103
- await setProxyMode(Modes.replay, sessionId);
103
+ page.on("close", async () => {
104
+ try {
105
+ await setProxyMode(Modes.replay, sessionId);
106
+ console.log(
107
+ `[Cleanup] Switched to transparent mode for session: ${sessionId}`
108
+ );
109
+ } catch (error) {
110
+ console.error("[Cleanup] Error during page close cleanup:", error);
111
+ }
112
+ });
104
113
  },
105
114
  /**
106
115
  * Global teardown - switches proxy to transparent mode
package/dist/proxy.js CHANGED
@@ -50,6 +50,7 @@ var HTTP_STATUS_OK = 200;
50
50
  var HTTP_STATUS_BAD_REQUEST = 400;
51
51
  var HTTP_STATUS_NOT_FOUND = 404;
52
52
  var CONTROL_ENDPOINT = "/__control";
53
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
53
54
 
54
55
  // src/types.ts
55
56
  var Modes = {
@@ -222,7 +223,7 @@ var ProxyServer = class {
222
223
  return {
223
224
  "access-control-allow-origin": origin || "*",
224
225
  "access-control-allow-credentials": "true",
225
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
226
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
226
227
  "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
227
228
  "access-control-expose-headers": "*"
228
229
  };
@@ -236,9 +237,22 @@ var ProxyServer = class {
236
237
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
237
238
  return target;
238
239
  }
240
+ /**
241
+ * Extract recording ID from custom HTTP header
242
+ * Used for concurrent replay session routing, especially with Next.js
243
+ * @param req The incoming HTTP request
244
+ * @returns The recording ID from header, or null if not found
245
+ */
246
+ getRecordingIdFromHeader(req) {
247
+ const headerValue = req.headers[RECORDING_ID_HEADER];
248
+ if (!headerValue) {
249
+ return null;
250
+ }
251
+ return Array.isArray(headerValue) ? headerValue[0] : headerValue;
252
+ }
239
253
  /**
240
254
  * Extract recording ID from request cookie
241
- * Used for concurrent replay session routing
255
+ * Used for concurrent replay session routing (fallback method)
242
256
  * @param req The incoming HTTP request
243
257
  * @returns The recording ID from cookie, or null if not found
244
258
  */
@@ -250,6 +264,14 @@ var ProxyServer = class {
250
264
  const match = cookies.match(/proxy-recording-id=([^;]+)/);
251
265
  return match ? decodeURIComponent(match[1]) : null;
252
266
  }
267
+ /**
268
+ * Extract recording ID from request using custom header (preferred) or cookie (fallback)
269
+ * @param req The incoming HTTP request
270
+ * @returns The recording ID, or null if not found
271
+ */
272
+ getRecordingIdFromRequest(req) {
273
+ return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
274
+ }
253
275
  /**
254
276
  * Get or create a replay session state for a given recording ID
255
277
  * @param recordingId The recording ID to get/create session for
@@ -527,7 +549,7 @@ var ProxyServer = class {
527
549
  return true;
528
550
  }
529
551
  async handleReplayRequest(req, res) {
530
- const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
552
+ const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
531
553
  if (!recordingId) {
532
554
  const corsHeaders = this.getCorsHeaders(req);
533
555
  res.writeHead(HTTP_STATUS_BAD_REQUEST, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -15,6 +15,11 @@
15
15
  "require": "./dist/playwright/index.cjs",
16
16
  "types": "./dist/playwright/index.d.ts",
17
17
  "default": "./dist/playwright/index.mjs"
18
+ },
19
+ "./nextjs": {
20
+ "require": "./dist/nextjs/index.cjs",
21
+ "types": "./dist/nextjs/index.d.ts",
22
+ "default": "./dist/nextjs/index.mjs"
18
23
  }
19
24
  },
20
25
  "bin": {