@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 +9 -0
- package/index.ts +465 -6
- 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
|
@@ -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:
|
|
270
|
-
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
|
|
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] ${
|
|
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"
|
|
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
|
@@ -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
|