@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 +9 -0
- package/index.ts +466 -8
- package/package.json +1 -1
- package/skills/rrweb-replay/SKILL.md +1 -0
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
|
|
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:
|
|
271
|
-
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
|
|
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] ${
|
|
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"
|
|
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
|
@@ -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
|