@rrwebcloud/openclaw-session-recording 2026.3.28-3 → 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
@@ -3,11 +3,10 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import {
6
- definePluginEntry,
7
6
  type AnyAgentTool,
8
7
  type OpenClawConfig,
9
8
  type OpenClawPluginApi,
10
- } from "openclaw/plugin-sdk/plugin-entry";
9
+ } from "openclaw/plugin-sdk";
11
10
 
12
11
  type RrwebReplayConfig = {
13
12
  enabled: boolean;
@@ -31,6 +30,8 @@ type ReplaySessionState = {
31
30
  currentProfile?: string;
32
31
  currentCdpUrl?: string;
33
32
  recordingPolicy: RrwebReplayConfig["recordingPolicy"];
33
+ armed?: boolean;
34
+ legacyMetadataUploaded?: boolean;
34
35
  status: "active" | "ended";
35
36
  reason?: string;
36
37
  updatedAt: string;
@@ -43,8 +44,12 @@ type ReplayStateStore = {
43
44
  const RRWEB_PLUGIN_ID = "rrweb-replay";
44
45
  const DEFAULT_RRWEB_API_BASE_URL = "https://api.rrwebcloud.com";
45
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";
46
49
  const LEGACY_BROWSER_RUNTIME_REASON =
47
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.";
48
53
 
49
54
  type BrowserRuntimeCompat = {
50
55
  clearManagedBrowserReplayContextsForSession: (sessionKey: string) => void;
@@ -252,6 +257,10 @@ function buildReplaySessionState(params: {
252
257
  currentProfile: params.previous?.currentProfile,
253
258
  currentCdpUrl: params.previous?.currentCdpUrl,
254
259
  recordingPolicy: params.config.recordingPolicy,
260
+ armed:
261
+ params.previous?.armed ??
262
+ (params.config.recordingPolicy === "browser-only" ? true : false),
263
+ legacyMetadataUploaded: params.previous?.legacyMetadataUploaded,
255
264
  status: "active",
256
265
  ...(params.reason ? { reason: params.reason } : {}),
257
266
  updatedAt,
@@ -267,8 +276,8 @@ function describeReplayAvailability(params: {
267
276
  }
268
277
  if (!params.browserRuntimeAvailable) {
269
278
  return {
270
- enabled: false,
271
- reason: LEGACY_BROWSER_RUNTIME_REASON,
279
+ enabled: true,
280
+ reason: LEGACY_DIRECT_INJECTION_REASON,
272
281
  };
273
282
  }
274
283
  const publicKey = resolveConfiguredSecret({
@@ -285,6 +294,233 @@ function describeReplayAvailability(params: {
285
294
  return { enabled: true };
286
295
  }
287
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
+
288
524
  async function loadBrowserRuntimeCompat(): Promise<BrowserRuntimeCompat | null> {
289
525
  try {
290
526
  const mod = (await import("openclaw/plugin-sdk/browser-runtime")) as BrowserRuntimeCompat;
@@ -348,6 +584,148 @@ function resolveConfiguredPublicKey(config: RrwebReplayConfig): string | undefin
348
584
  });
349
585
  }
350
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
+
351
729
  async function upsertReplaySession(params: {
352
730
  api: OpenClawPluginApi;
353
731
  config: RrwebReplayConfig;
@@ -450,7 +828,24 @@ async function armReplayContext(params: {
450
828
  preferredProfile: params.preferredProfile ?? replayEntry.currentProfile,
451
829
  });
452
830
  if (!params.browserRuntime || !profile || !profile.cdpUrl) {
453
- 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
+ });
454
849
  }
455
850
  params.browserRuntime.setManagedBrowserReplayContext({
456
851
  cdpUrl: profile.cdpUrl,
@@ -474,6 +869,7 @@ async function armReplayContext(params: {
474
869
  replaySessionId: replayEntry.replaySessionId,
475
870
  currentProfile: profile.name,
476
871
  currentCdpUrl: profile.cdpUrl,
872
+ armed: true,
477
873
  updatedAt: new Date().toISOString(),
478
874
  }),
479
875
  });
@@ -585,7 +981,7 @@ const rrwebReplayPlugin = {
585
981
  return;
586
982
  }
587
983
  if (!browserRuntime) {
588
- ctx.logger.warn(`[rrweb-replay] ${LEGACY_BROWSER_RUNTIME_REASON}`);
984
+ ctx.logger.warn(`[rrweb-replay] ${LEGACY_DIRECT_INJECTION_REASON}`);
589
985
  return;
590
986
  }
591
987
  if (!extensionDir) {
@@ -657,7 +1053,7 @@ const rrwebReplayPlugin = {
657
1053
  config: pluginConfig,
658
1054
  browserRuntimeAvailable: Boolean(browserRuntime),
659
1055
  });
660
- if (event.toolName !== "browser" || pluginConfig.recordingPolicy !== "browser-only") {
1056
+ if (event.toolName !== "browser") {
661
1057
  return;
662
1058
  }
663
1059
  if (!availability.enabled) {
@@ -667,7 +1063,7 @@ const rrwebReplayPlugin = {
667
1063
  typeof event.params.profile === "string" && event.params.profile.trim()
668
1064
  ? event.params.profile.trim()
669
1065
  : undefined;
670
- await armReplayContext({
1066
+ const replayEntry = await armReplayContext({
671
1067
  api,
672
1068
  pluginConfig,
673
1069
  browserRuntime,
@@ -675,6 +1071,59 @@ const rrwebReplayPlugin = {
675
1071
  sessionId: ctx.sessionId,
676
1072
  preferredProfile: rawProfile,
677
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
+ });
678
1127
  });
679
1128
 
680
1129
  api.registerTool(
@@ -693,4 +1142,13 @@ const rrwebReplayPlugin = {
693
1142
  },
694
1143
  };
695
1144
 
1145
+ export const __testing = {
1146
+ buildLegacyReplayInstallScript,
1147
+ buildLegacyReplayFlushScript,
1148
+ buildLegacyReplayIngestUrl,
1149
+ buildLegacyReplayPreviewUrl,
1150
+ resolveBrowserControlBaseUrl,
1151
+ resolveBrowserControlHeaders,
1152
+ };
1153
+
696
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-3",
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