@rrwebcloud/openclaw-session-recording 2026.3.28-2 → 2026.3.28-4

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
@@ -8,6 +8,14 @@ Portable rrweb replay plugin for OpenClaw-managed browsers.
8
8
  openclaw plugins install @rrwebcloud/openclaw-session-recording
9
9
  ```
10
10
 
11
+ If you are working from this repository checkout instead of the published
12
+ package, use:
13
+
14
+ ```bash
15
+ openclaw plugins install -l ./extensions/rrweb-replay
16
+ openclaw plugins install -l ./extensions/diagnostics-otel
17
+ ```
18
+
11
19
  ## Minimal config
12
20
 
13
21
  ```yaml
@@ -29,3 +37,4 @@ plugins:
29
37
  - the browser extension is optional for the runtime uploader path and is only needed when you specifically want extension-side behavior
30
38
  - the plugin targets managed Chromium browser profiles such as `openclaw`
31
39
  - `replay_session_info` exposes the active replay metadata
40
+ - When `diagnostics-otel` is enabled, replay-aware spans include `openclaw.replay.*` attributes. See [Logging](../../docs/logging.md).
package/index.ts CHANGED
@@ -30,6 +30,8 @@ type ReplaySessionState = {
30
30
  currentProfile?: string;
31
31
  currentCdpUrl?: string;
32
32
  recordingPolicy: RrwebReplayConfig["recordingPolicy"];
33
+ armed?: boolean;
34
+ legacyMetadataUploaded?: boolean;
33
35
  status: "active" | "ended";
34
36
  reason?: string;
35
37
  updatedAt: string;
@@ -42,8 +44,12 @@ type ReplayStateStore = {
42
44
  const RRWEB_PLUGIN_ID = "rrweb-replay";
43
45
  const DEFAULT_RRWEB_API_BASE_URL = "https://api.rrwebcloud.com";
44
46
  const DEFAULT_RRWEB_CDN_URL = "https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js";
47
+ const DEFAULT_GATEWAY_PORT = 18789;
48
+ const LEGACY_REPLAY_WINDOW_KEY = "__openclawRrwebLegacyRecorder";
45
49
  const LEGACY_BROWSER_RUNTIME_REASON =
46
50
  "This OpenClaw host does not export plugin-sdk/browser-runtime. Automatic rrweb extension wiring requires OpenClaw >=2026.3.24. On legacy hosts, install/configure the rrweb browser addon separately or upgrade OpenClaw.";
51
+ const LEGACY_DIRECT_INJECTION_REASON =
52
+ "Using legacy direct page injection because plugin-sdk/browser-runtime is unavailable on this host.";
47
53
 
48
54
  type BrowserRuntimeCompat = {
49
55
  clearManagedBrowserReplayContextsForSession: (sessionKey: string) => void;
@@ -251,6 +257,10 @@ function buildReplaySessionState(params: {
251
257
  currentProfile: params.previous?.currentProfile,
252
258
  currentCdpUrl: params.previous?.currentCdpUrl,
253
259
  recordingPolicy: params.config.recordingPolicy,
260
+ armed:
261
+ params.previous?.armed ??
262
+ (params.config.recordingPolicy === "browser-only" ? true : false),
263
+ legacyMetadataUploaded: params.previous?.legacyMetadataUploaded,
254
264
  status: "active",
255
265
  ...(params.reason ? { reason: params.reason } : {}),
256
266
  updatedAt,
@@ -266,8 +276,8 @@ function describeReplayAvailability(params: {
266
276
  }
267
277
  if (!params.browserRuntimeAvailable) {
268
278
  return {
269
- enabled: false,
270
- reason: LEGACY_BROWSER_RUNTIME_REASON,
279
+ enabled: true,
280
+ reason: LEGACY_DIRECT_INJECTION_REASON,
271
281
  };
272
282
  }
273
283
  const publicKey = resolveConfiguredSecret({
@@ -284,6 +294,233 @@ function describeReplayAvailability(params: {
284
294
  return { enabled: true };
285
295
  }
286
296
 
297
+ function isFinitePositiveInteger(value: unknown): value is number {
298
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
299
+ }
300
+
301
+ function resolveGatewayPort(config: OpenClawConfig): number {
302
+ const envPort = Number.parseInt(process.env.OPENCLAW_GATEWAY_PORT ?? "", 10);
303
+ if (Number.isFinite(envPort) && envPort > 0) {
304
+ return envPort;
305
+ }
306
+ const gateway = (config as { gateway?: { port?: unknown } }).gateway;
307
+ if (isFinitePositiveInteger(gateway?.port)) {
308
+ return Math.floor(gateway.port);
309
+ }
310
+ return DEFAULT_GATEWAY_PORT;
311
+ }
312
+
313
+ function resolveBrowserControlBaseUrl(config: OpenClawConfig): string {
314
+ return `http://127.0.0.1:${resolveGatewayPort(config) + 2}`;
315
+ }
316
+
317
+ function resolveBrowserControlHeaders(config: OpenClawConfig): Record<string, string> {
318
+ const headers: Record<string, string> = { "content-type": "application/json" };
319
+ const auth = (config as { gateway?: { auth?: Record<string, unknown> } }).gateway?.auth;
320
+ if (!auth || typeof auth !== "object") {
321
+ return headers;
322
+ }
323
+ const token = typeof auth.token === "string" ? auth.token.trim() : "";
324
+ if (token) {
325
+ headers.authorization = `Bearer ${token}`;
326
+ return headers;
327
+ }
328
+ const password = typeof auth.password === "string" ? auth.password.trim() : "";
329
+ if (password) {
330
+ headers["x-openclaw-password"] = password;
331
+ }
332
+ return headers;
333
+ }
334
+
335
+ async function fetchJsonWithTimeout<T>(
336
+ url: string,
337
+ init: RequestInit & { timeoutMs?: number },
338
+ ): Promise<T> {
339
+ const ctrl = new AbortController();
340
+ const timer = setTimeout(() => ctrl.abort(new Error("timed out")), init.timeoutMs ?? 20_000);
341
+ try {
342
+ const response = await fetch(url, { ...init, signal: ctrl.signal });
343
+ if (!response.ok) {
344
+ const body = await response.text().catch(() => "");
345
+ throw new Error(body || `${response.status} ${response.statusText}`);
346
+ }
347
+ return (await response.json()) as T;
348
+ } finally {
349
+ clearTimeout(timer);
350
+ }
351
+ }
352
+
353
+ function buildBrowserControlUrl(config: OpenClawConfig, route: string, profile?: string): string {
354
+ const url = new URL(route, `${resolveBrowserControlBaseUrl(config)}/`);
355
+ if (profile?.trim()) {
356
+ url.searchParams.set("profile", profile.trim());
357
+ }
358
+ return url.toString();
359
+ }
360
+
361
+ function buildLegacyReplayPreviewUrl(serverUrl: string | undefined, replaySessionId: string): string {
362
+ return `${normalizeServerUrl(serverUrl) ?? DEFAULT_RRWEB_API_BASE_URL}/recordings/${replaySessionId}/preview`;
363
+ }
364
+
365
+ function buildLegacyReplayIngestUrl(serverUrl: string | undefined, replaySessionId: string): string {
366
+ return `${normalizeServerUrl(serverUrl) ?? DEFAULT_RRWEB_API_BASE_URL}/recordings/${replaySessionId}/ingest`;
367
+ }
368
+
369
+ function buildLegacyReplayMetadataEvent(params: {
370
+ sessionKey?: string;
371
+ sessionId?: string;
372
+ replaySessionId: string;
373
+ }): Record<string, unknown> {
374
+ return {
375
+ timestamp: Date.now(),
376
+ type: 5,
377
+ data: {
378
+ tag: "recording-meta",
379
+ payload: {
380
+ session_key: params.sessionKey,
381
+ session_id: params.sessionId,
382
+ replay_session_id: params.replaySessionId,
383
+ },
384
+ },
385
+ };
386
+ }
387
+
388
+ function buildLegacyReplayInstallScript(params: {
389
+ replaySessionId: string;
390
+ replayUrl: string;
391
+ rrwebCdnUrl: string;
392
+ }): string {
393
+ return `async () => {
394
+ const stateKey = ${JSON.stringify(LEGACY_REPLAY_WINDOW_KEY)};
395
+ const rrwebCdnUrl = ${JSON.stringify(params.rrwebCdnUrl)};
396
+ const replaySessionId = ${JSON.stringify(params.replaySessionId)};
397
+ const replayUrl = ${JSON.stringify(params.replayUrl)};
398
+ const state = window[stateKey] && typeof window[stateKey] === "object" ? window[stateKey] : {};
399
+ window[stateKey] = state;
400
+ state.recordingId = replaySessionId;
401
+ state.previewUrl = replayUrl;
402
+ state.queue = Array.isArray(state.queue) ? state.queue : [];
403
+ const ensureScript = () => new Promise((resolve, reject) => {
404
+ if (window.rrweb && typeof window.rrweb.record === "function") {
405
+ resolve();
406
+ return;
407
+ }
408
+ const selector = 'script[data-openclaw-rrweb="' + rrwebCdnUrl + '"]';
409
+ const existing = document.querySelector(selector);
410
+ if (existing) {
411
+ existing.addEventListener("load", () => resolve(), { once: true });
412
+ existing.addEventListener("error", () => reject(new Error("rrweb script failed to load")), { once: true });
413
+ return;
414
+ }
415
+ const script = document.createElement("script");
416
+ script.src = rrwebCdnUrl;
417
+ script.async = true;
418
+ script.dataset.openclawRrweb = rrwebCdnUrl;
419
+ script.onload = () => resolve();
420
+ script.onerror = () => reject(new Error("rrweb script failed to load"));
421
+ const parent = document.head || document.body || document.documentElement;
422
+ if (!parent) {
423
+ reject(new Error("No DOM parent available for rrweb script"));
424
+ return;
425
+ }
426
+ parent.appendChild(script);
427
+ });
428
+ if (!state.started) {
429
+ if (document.readyState === "loading") {
430
+ await new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }));
431
+ }
432
+ await ensureScript();
433
+ if (!window.rrweb || typeof window.rrweb.record !== "function") {
434
+ throw new Error("rrweb recorder unavailable");
435
+ }
436
+ state.stop = window.rrweb.record({
437
+ emit(event) {
438
+ state.queue.push(event);
439
+ },
440
+ recordCanvas: false,
441
+ collectFonts: false,
442
+ });
443
+ state.started = true;
444
+ }
445
+ window.__OPENCLAW_RRWEB_SESSION__ = {
446
+ sessionId: replaySessionId,
447
+ url: replayUrl,
448
+ provider: "rrwebcloud",
449
+ };
450
+ return {
451
+ ok: true,
452
+ replaySessionId,
453
+ replayUrl,
454
+ queued: Array.isArray(state.queue) ? state.queue.length : 0,
455
+ };
456
+ }`;
457
+ }
458
+
459
+ function buildLegacyReplayFlushScript(): string {
460
+ return `() => {
461
+ const state = window[${JSON.stringify(LEGACY_REPLAY_WINDOW_KEY)}];
462
+ if (!state || typeof state !== "object") {
463
+ return { ok: false, events: [] };
464
+ }
465
+ const queue = Array.isArray(state.queue) ? state.queue : [];
466
+ const events = queue.splice(0, queue.length);
467
+ return {
468
+ ok: true,
469
+ replaySessionId: typeof state.recordingId === "string" ? state.recordingId : undefined,
470
+ replayUrl: typeof state.previewUrl === "string" ? state.previewUrl : undefined,
471
+ events,
472
+ };
473
+ }`;
474
+ }
475
+
476
+ async function browserControlActEvaluate(params: {
477
+ config: OpenClawConfig;
478
+ profile: string;
479
+ targetId?: string;
480
+ fn: string;
481
+ }): Promise<{ ok?: boolean; result?: unknown; targetId?: string; url?: string }> {
482
+ return await fetchJsonWithTimeout(
483
+ buildBrowserControlUrl(params.config, "/act", params.profile),
484
+ {
485
+ method: "POST",
486
+ headers: resolveBrowserControlHeaders(params.config),
487
+ body: JSON.stringify({
488
+ kind: "evaluate",
489
+ fn: params.fn,
490
+ ...(params.targetId ? { targetId: params.targetId } : {}),
491
+ }),
492
+ },
493
+ );
494
+ }
495
+
496
+ function resolveBrowserTargetId(params: {
497
+ eventParams: Record<string, unknown>;
498
+ result?: unknown;
499
+ }): string | undefined {
500
+ const topLevel = typeof params.eventParams.targetId === "string" ? params.eventParams.targetId.trim() : "";
501
+ if (topLevel) {
502
+ return topLevel;
503
+ }
504
+ const nestedRequest =
505
+ params.eventParams.request && typeof params.eventParams.request === "object" && !Array.isArray(params.eventParams.request)
506
+ ? (params.eventParams.request as Record<string, unknown>)
507
+ : null;
508
+ const nested = typeof nestedRequest?.targetId === "string" ? nestedRequest.targetId.trim() : "";
509
+ if (nested) {
510
+ return nested;
511
+ }
512
+ if (params.result && typeof params.result === "object" && !Array.isArray(params.result)) {
513
+ const resultTargetId =
514
+ typeof (params.result as Record<string, unknown>).targetId === "string"
515
+ ? ((params.result as Record<string, unknown>).targetId as string).trim()
516
+ : "";
517
+ if (resultTargetId) {
518
+ return resultTargetId;
519
+ }
520
+ }
521
+ return undefined;
522
+ }
523
+
287
524
  async function loadBrowserRuntimeCompat(): Promise<BrowserRuntimeCompat | null> {
288
525
  try {
289
526
  const mod = (await import("openclaw/plugin-sdk/browser-runtime")) as BrowserRuntimeCompat;
@@ -347,6 +584,148 @@ function resolveConfiguredPublicKey(config: RrwebReplayConfig): string | undefin
347
584
  });
348
585
  }
349
586
 
587
+ async function ensureLegacyReplayCapture(params: {
588
+ api: OpenClawPluginApi;
589
+ pluginConfig: RrwebReplayConfig;
590
+ entry: ReplaySessionState;
591
+ profile: string;
592
+ targetId?: string;
593
+ }): Promise<ReplaySessionState> {
594
+ const publicKey = resolveConfiguredPublicKey(params.pluginConfig);
595
+ if (!publicKey) {
596
+ return params.entry;
597
+ }
598
+ const replayUrl =
599
+ params.entry.replayUrl ??
600
+ buildLegacyReplayPreviewUrl(params.entry.replayServerUrl, params.entry.replaySessionId);
601
+ const response = await browserControlActEvaluate({
602
+ config: params.api.config,
603
+ profile: params.profile,
604
+ targetId: params.targetId,
605
+ fn: buildLegacyReplayInstallScript({
606
+ replaySessionId: params.entry.replaySessionId,
607
+ replayUrl,
608
+ rrwebCdnUrl: DEFAULT_RRWEB_CDN_URL,
609
+ }),
610
+ });
611
+ const payload =
612
+ response.result && typeof response.result === "object" && !Array.isArray(response.result)
613
+ ? (response.result as Record<string, unknown>)
614
+ : null;
615
+ const replaySessionId =
616
+ typeof payload?.replaySessionId === "string" && payload.replaySessionId.trim()
617
+ ? payload.replaySessionId.trim()
618
+ : params.entry.replaySessionId;
619
+ const nextReplayUrl =
620
+ typeof payload?.replayUrl === "string" && payload.replayUrl.trim()
621
+ ? payload.replayUrl.trim()
622
+ : replayUrl;
623
+ return await upsertReplaySession({
624
+ api: params.api,
625
+ config: params.pluginConfig,
626
+ sessionKey: params.entry.sessionKey,
627
+ sessionId: params.entry.sessionId,
628
+ mutate: (current) => ({
629
+ ...current,
630
+ replaySessionId,
631
+ replayUrl: nextReplayUrl,
632
+ currentProfile: params.profile,
633
+ armed: true,
634
+ reason: LEGACY_DIRECT_INJECTION_REASON,
635
+ updatedAt: new Date().toISOString(),
636
+ }),
637
+ });
638
+ }
639
+
640
+ async function flushLegacyReplayCapture(params: {
641
+ api: OpenClawPluginApi;
642
+ pluginConfig: RrwebReplayConfig;
643
+ entry: ReplaySessionState;
644
+ profile: string;
645
+ targetId?: string;
646
+ }): Promise<ReplaySessionState> {
647
+ const publicKey = resolveConfiguredPublicKey(params.pluginConfig);
648
+ if (!publicKey) {
649
+ return params.entry;
650
+ }
651
+ const response = await browserControlActEvaluate({
652
+ config: params.api.config,
653
+ profile: params.profile,
654
+ targetId: params.targetId,
655
+ fn: buildLegacyReplayFlushScript(),
656
+ }).catch(() => null);
657
+ const payload =
658
+ response?.result && typeof response.result === "object" && !Array.isArray(response.result)
659
+ ? (response.result as Record<string, unknown>)
660
+ : null;
661
+ const replaySessionId =
662
+ typeof payload?.replaySessionId === "string" && payload.replaySessionId.trim()
663
+ ? payload.replaySessionId.trim()
664
+ : params.entry.replaySessionId;
665
+ const replayUrl =
666
+ typeof payload?.replayUrl === "string" && payload.replayUrl.trim()
667
+ ? payload.replayUrl.trim()
668
+ : params.entry.replayUrl ??
669
+ buildLegacyReplayPreviewUrl(params.entry.replayServerUrl, replaySessionId);
670
+ const events = Array.isArray(payload?.events)
671
+ ? payload.events.filter(
672
+ (event): event is Record<string, unknown> =>
673
+ Boolean(event && typeof event === "object" && !Array.isArray(event)),
674
+ )
675
+ : [];
676
+ if (events.length === 0 && params.entry.legacyMetadataUploaded) {
677
+ return params.entry;
678
+ }
679
+ const lines: string[] = [];
680
+ if (!params.entry.legacyMetadataUploaded) {
681
+ lines.push(
682
+ JSON.stringify(
683
+ buildLegacyReplayMetadataEvent({
684
+ sessionKey: params.entry.sessionKey,
685
+ sessionId: params.entry.sessionId,
686
+ replaySessionId,
687
+ }),
688
+ ),
689
+ );
690
+ }
691
+ for (const event of events) {
692
+ lines.push(JSON.stringify(event));
693
+ }
694
+ const ingestResponse = await fetch(
695
+ buildLegacyReplayIngestUrl(params.entry.replayServerUrl, replaySessionId),
696
+ {
697
+ method: "POST",
698
+ headers: {
699
+ authorization: `Bearer ${publicKey}`,
700
+ "content-type": "application/x-ndjson",
701
+ },
702
+ body: lines.join("\n"),
703
+ },
704
+ );
705
+ if (!ingestResponse.ok) {
706
+ const body = await ingestResponse.text().catch(() => "");
707
+ throw new Error(
708
+ `rrwebcloud ingest failed (${ingestResponse.status}): ${body || ingestResponse.statusText}`,
709
+ );
710
+ }
711
+ return await upsertReplaySession({
712
+ api: params.api,
713
+ config: params.pluginConfig,
714
+ sessionKey: params.entry.sessionKey,
715
+ sessionId: params.entry.sessionId,
716
+ mutate: (current) => ({
717
+ ...current,
718
+ replaySessionId,
719
+ replayUrl,
720
+ currentProfile: params.profile,
721
+ armed: true,
722
+ legacyMetadataUploaded: true,
723
+ reason: LEGACY_DIRECT_INJECTION_REASON,
724
+ updatedAt: new Date().toISOString(),
725
+ }),
726
+ });
727
+ }
728
+
350
729
  async function upsertReplaySession(params: {
351
730
  api: OpenClawPluginApi;
352
731
  config: RrwebReplayConfig;
@@ -449,7 +828,24 @@ async function armReplayContext(params: {
449
828
  preferredProfile: params.preferredProfile ?? replayEntry.currentProfile,
450
829
  });
451
830
  if (!params.browserRuntime || !profile || !profile.cdpUrl) {
452
- return replayEntry;
831
+ return await upsertReplaySession({
832
+ api: params.api,
833
+ config: params.pluginConfig,
834
+ sessionKey: params.sessionKey,
835
+ sessionId: params.sessionId,
836
+ mutate: (entry) => ({
837
+ ...entry,
838
+ replaySessionId: replayEntry.replaySessionId,
839
+ replayUrl:
840
+ replayEntry.replayUrl ??
841
+ buildLegacyReplayPreviewUrl(replayEntry.replayServerUrl, replayEntry.replaySessionId),
842
+ currentProfile: profile?.name ?? entry.currentProfile,
843
+ currentCdpUrl: profile?.cdpUrl ?? entry.currentCdpUrl,
844
+ armed: true,
845
+ reason: LEGACY_DIRECT_INJECTION_REASON,
846
+ updatedAt: new Date().toISOString(),
847
+ }),
848
+ });
453
849
  }
454
850
  params.browserRuntime.setManagedBrowserReplayContext({
455
851
  cdpUrl: profile.cdpUrl,
@@ -473,6 +869,7 @@ async function armReplayContext(params: {
473
869
  replaySessionId: replayEntry.replaySessionId,
474
870
  currentProfile: profile.name,
475
871
  currentCdpUrl: profile.cdpUrl,
872
+ armed: true,
476
873
  updatedAt: new Date().toISOString(),
477
874
  }),
478
875
  });
@@ -584,7 +981,7 @@ const rrwebReplayPlugin = {
584
981
  return;
585
982
  }
586
983
  if (!browserRuntime) {
587
- ctx.logger.warn(`[rrweb-replay] ${LEGACY_BROWSER_RUNTIME_REASON}`);
984
+ ctx.logger.warn(`[rrweb-replay] ${LEGACY_DIRECT_INJECTION_REASON}`);
588
985
  return;
589
986
  }
590
987
  if (!extensionDir) {
@@ -656,7 +1053,7 @@ const rrwebReplayPlugin = {
656
1053
  config: pluginConfig,
657
1054
  browserRuntimeAvailable: Boolean(browserRuntime),
658
1055
  });
659
- if (event.toolName !== "browser" || pluginConfig.recordingPolicy !== "browser-only") {
1056
+ if (event.toolName !== "browser") {
660
1057
  return;
661
1058
  }
662
1059
  if (!availability.enabled) {
@@ -666,7 +1063,7 @@ const rrwebReplayPlugin = {
666
1063
  typeof event.params.profile === "string" && event.params.profile.trim()
667
1064
  ? event.params.profile.trim()
668
1065
  : undefined;
669
- await armReplayContext({
1066
+ const replayEntry = await armReplayContext({
670
1067
  api,
671
1068
  pluginConfig,
672
1069
  browserRuntime,
@@ -674,6 +1071,59 @@ const rrwebReplayPlugin = {
674
1071
  sessionId: ctx.sessionId,
675
1072
  preferredProfile: rawProfile,
676
1073
  });
1074
+ if (browserRuntime || !replayEntry.armed) {
1075
+ return;
1076
+ }
1077
+ const targetId = resolveBrowserTargetId({ eventParams: event.params });
1078
+ const profile =
1079
+ rawProfile || replayEntry.currentProfile || pluginConfig.browserProfiles[0] || "openclaw";
1080
+ await ensureLegacyReplayCapture({
1081
+ api,
1082
+ pluginConfig,
1083
+ entry: replayEntry,
1084
+ profile,
1085
+ targetId,
1086
+ }).catch(() => undefined);
1087
+ });
1088
+
1089
+ api.on("after_tool_call", async (event, ctx) => {
1090
+ const browserRuntime = await browserRuntimePromise;
1091
+ if (event.toolName !== "browser" || browserRuntime) {
1092
+ return;
1093
+ }
1094
+ const replayEntry = await getReplaySessionEntry({
1095
+ api,
1096
+ sessionKey: ctx.sessionKey,
1097
+ sessionId: ctx.sessionId,
1098
+ });
1099
+ if (!replayEntry?.armed) {
1100
+ return;
1101
+ }
1102
+ const profile =
1103
+ (typeof event.params.profile === "string" && event.params.profile.trim()) ||
1104
+ replayEntry.currentProfile ||
1105
+ pluginConfig.browserProfiles[0] ||
1106
+ "openclaw";
1107
+ const targetId = resolveBrowserTargetId({
1108
+ eventParams: event.params,
1109
+ result: event.result,
1110
+ });
1111
+ const prepared = await ensureLegacyReplayCapture({
1112
+ api,
1113
+ pluginConfig,
1114
+ entry: replayEntry,
1115
+ profile,
1116
+ targetId,
1117
+ }).catch(() => replayEntry);
1118
+ await flushLegacyReplayCapture({
1119
+ api,
1120
+ pluginConfig,
1121
+ entry: prepared,
1122
+ profile,
1123
+ targetId,
1124
+ }).catch((err) => {
1125
+ api.logger.warn(`[rrweb-replay] legacy capture flush failed: ${String(err)}`);
1126
+ });
677
1127
  });
678
1128
 
679
1129
  api.registerTool(
@@ -692,4 +1142,13 @@ const rrwebReplayPlugin = {
692
1142
  },
693
1143
  };
694
1144
 
1145
+ export const __testing = {
1146
+ buildLegacyReplayInstallScript,
1147
+ buildLegacyReplayFlushScript,
1148
+ buildLegacyReplayIngestUrl,
1149
+ buildLegacyReplayPreviewUrl,
1150
+ resolveBrowserControlBaseUrl,
1151
+ resolveBrowserControlHeaders,
1152
+ };
1153
+
695
1154
  export default rrwebReplayPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rrwebcloud/openclaw-session-recording",
3
- "version": "2026.3.28-2",
3
+ "version": "2026.3.28-4",
4
4
  "description": "OpenClaw rrweb replay plugin",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -16,6 +16,7 @@ Operational notes:
16
16
  - The plugin targets OpenClaw-managed Chromium profiles such as `openclaw`
17
17
  - The streamlined OpenClaw path records rrweb events in the managed browser and uploads authenticated NDJSON from runtime code
18
18
  - The streamlined OpenClaw path only needs a public key; the rrweb Cloud API endpoint defaults to `https://api.rrwebcloud.com` and recording does not require a secret key
19
+ - When `diagnostics-otel` is enabled, replay metadata is also exported on spans as `openclaw.replay.*`
19
20
  - Minimal config:
20
21
 
21
22
  ```yaml