@agent-native/core 0.98.7 → 0.98.8
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/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/docs/content/external-agents.mdx +17 -11
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +6 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +29 -7
- package/corpus/core/src/integrations/adapters/slack.ts +13 -2
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +2 -1
- package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
- package/corpus/core/src/integrations/plugin.ts +183 -48
- package/corpus/core/src/integrations/types.ts +7 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +24 -7
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/integrations/adapters/slack.js +11 -2
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +2 -1
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.js +1 -1
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +140 -34
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +7 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/docs/content/external-agents.mdx +17 -11
- package/docs/content/locales/ar-SA/external-agents.mdx +6 -2
- package/docs/content/locales/de-DE/external-agents.mdx +6 -2
- package/docs/content/locales/es-ES/external-agents.mdx +6 -2
- package/docs/content/locales/fr-FR/external-agents.mdx +6 -2
- package/docs/content/locales/hi-IN/external-agents.mdx +6 -2
- package/docs/content/locales/ja-JP/external-agents.mdx +6 -2
- package/docs/content/locales/ko-KR/external-agents.mdx +6 -2
- package/docs/content/locales/pt-BR/external-agents.mdx +6 -2
- package/docs/content/locales/zh-CN/external-agents.mdx +6 -2
- package/docs/content/locales/zh-TW/external-agents.mdx +6 -2
- package/package.json +1 -1
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
claimPendingTask,
|
|
81
81
|
getNextPendingTaskIdForThread,
|
|
82
82
|
insertPendingTask,
|
|
83
|
+
isDuplicateEventError,
|
|
83
84
|
MAX_PENDING_TASK_ATTEMPTS,
|
|
84
85
|
markTaskCompleted,
|
|
85
86
|
markTaskFailed,
|
|
@@ -140,6 +141,7 @@ import type {
|
|
|
140
141
|
IntegrationsPluginOptions,
|
|
141
142
|
IntegrationStatus,
|
|
142
143
|
IntegrationExecutionContext,
|
|
144
|
+
IncomingMessage,
|
|
143
145
|
} from "./types.js";
|
|
144
146
|
import {
|
|
145
147
|
listIntegrationUsageBudgets,
|
|
@@ -259,6 +261,23 @@ const REMOTE_DEVICE_ONLINE_MS = 90_000;
|
|
|
259
261
|
// API outage every message would otherwise get another identical "try again"
|
|
260
262
|
// reply. Short enough that a persistent condition still reminds the sender.
|
|
261
263
|
const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
|
|
264
|
+
const SYSTEM_NOTICE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
265
|
+
|
|
266
|
+
type IntegrationSystemNoticeTaskPayload = {
|
|
267
|
+
kind: "system-notice";
|
|
268
|
+
incoming: IncomingMessage;
|
|
269
|
+
text: string;
|
|
270
|
+
dedupeKey?: string;
|
|
271
|
+
dedupeTtlMs?: number;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
function systemNoticeEventKey(
|
|
275
|
+
dedupeKey: string,
|
|
276
|
+
ttlMs: number,
|
|
277
|
+
now = Date.now(),
|
|
278
|
+
): string {
|
|
279
|
+
return `system-notice:${dedupeKey}:${Math.floor(now / ttlMs)}`;
|
|
280
|
+
}
|
|
262
281
|
|
|
263
282
|
export async function enqueueRemoteCommand(
|
|
264
283
|
envelope: RemoteCodeCommandEnvelope,
|
|
@@ -637,6 +656,66 @@ export function createIntegrationsPlugin(
|
|
|
637
656
|
const h3 = getH3App(nitroApp);
|
|
638
657
|
const P = `${FRAMEWORK_ROUTE_PREFIX}/integrations`;
|
|
639
658
|
|
|
659
|
+
async function enqueueSystemNotice(
|
|
660
|
+
event: any,
|
|
661
|
+
incoming: IncomingMessage,
|
|
662
|
+
text: string,
|
|
663
|
+
opts?: { dedupeKey?: string; dedupeTtlMs?: number },
|
|
664
|
+
): Promise<void> {
|
|
665
|
+
if (!text.trim()) return;
|
|
666
|
+
const taskId = `notice-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
667
|
+
const dedupeTtlMs = Math.max(
|
|
668
|
+
1,
|
|
669
|
+
opts?.dedupeTtlMs ?? SYSTEM_NOTICE_DEDUPE_TTL_MS,
|
|
670
|
+
);
|
|
671
|
+
const noticeThreadId = `system-notice:${taskId}`;
|
|
672
|
+
const payload: IntegrationSystemNoticeTaskPayload = {
|
|
673
|
+
kind: "system-notice",
|
|
674
|
+
incoming,
|
|
675
|
+
text,
|
|
676
|
+
...(opts?.dedupeKey ? { dedupeKey: opts.dedupeKey } : {}),
|
|
677
|
+
...(opts?.dedupeTtlMs ? { dedupeTtlMs: opts.dedupeTtlMs } : {}),
|
|
678
|
+
};
|
|
679
|
+
try {
|
|
680
|
+
await insertPendingTask({
|
|
681
|
+
id: taskId,
|
|
682
|
+
platform: incoming.platform,
|
|
683
|
+
// System notices are auxiliary delivery work, not the user's agent
|
|
684
|
+
// run. Give each notice its own queue lane so a retrying notice cannot
|
|
685
|
+
// block the real message task for this Slack/Telegram thread.
|
|
686
|
+
externalThreadId: noticeThreadId,
|
|
687
|
+
payload: JSON.stringify(payload),
|
|
688
|
+
ownerEmail: `integration@${incoming.platform}`,
|
|
689
|
+
externalEventKey: opts?.dedupeKey
|
|
690
|
+
? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
|
|
691
|
+
: undefined,
|
|
692
|
+
});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
if (isDuplicateEventError(err)) return;
|
|
695
|
+
throw err;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// The SQL row is the durable source of truth. This best-effort self-call
|
|
699
|
+
// only reduces latency; the recurring pending-task sweep retries a row
|
|
700
|
+
// if the serverless host freezes this webhook execution immediately.
|
|
701
|
+
let token: string | undefined;
|
|
702
|
+
try {
|
|
703
|
+
token = signInternalToken(taskId);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
if (process.env.NODE_ENV === "production") throw err;
|
|
706
|
+
}
|
|
707
|
+
void fetch(`${getBaseUrl(event)}${P}/process-task`, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
"Content-Type": "application/json",
|
|
711
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify({ taskId }),
|
|
714
|
+
}).catch((err) => {
|
|
715
|
+
console.warn("[integrations] System notice dispatch failed:", err);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
640
719
|
async function requireSession(event: any): Promise<boolean> {
|
|
641
720
|
const session = await getSession(event).catch(() => null);
|
|
642
721
|
if (session?.email) return true;
|
|
@@ -1550,6 +1629,34 @@ export function createIntegrationsPlugin(
|
|
|
1550
1629
|
isIntegrationCaller: true,
|
|
1551
1630
|
},
|
|
1552
1631
|
async () => {
|
|
1632
|
+
const taskPayload = JSON.parse(task.payload) as
|
|
1633
|
+
| IntegrationSystemNoticeTaskPayload
|
|
1634
|
+
| { kind?: undefined };
|
|
1635
|
+
if (taskPayload.kind === "system-notice") {
|
|
1636
|
+
if (!adapter.sendSystemNotice) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`Platform ${task.platform} cannot deliver system notices`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
const config = await getIntegrationConfig(task.platform);
|
|
1642
|
+
const credentialContext =
|
|
1643
|
+
await credentialContextForIntegrationConfig(config);
|
|
1644
|
+
await withCredentialContext(credentialContext, () =>
|
|
1645
|
+
adapter.sendSystemNotice!(
|
|
1646
|
+
taskPayload.incoming,
|
|
1647
|
+
taskPayload.text,
|
|
1648
|
+
{
|
|
1649
|
+
...(taskPayload.dedupeKey
|
|
1650
|
+
? { dedupeKey: taskPayload.dedupeKey }
|
|
1651
|
+
: {}),
|
|
1652
|
+
...(taskPayload.dedupeTtlMs
|
|
1653
|
+
? { dedupeTtlMs: taskPayload.dedupeTtlMs }
|
|
1654
|
+
: {}),
|
|
1655
|
+
},
|
|
1656
|
+
),
|
|
1657
|
+
);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1553
1660
|
const resources = await loadResourcesForPrompt(
|
|
1554
1661
|
task.ownerEmail,
|
|
1555
1662
|
true,
|
|
@@ -2378,7 +2485,8 @@ export function createIntegrationsPlugin(
|
|
|
2378
2485
|
null;
|
|
2379
2486
|
if (
|
|
2380
2487
|
incoming.platform === "slack" &&
|
|
2381
|
-
incoming.conversationType === "dm"
|
|
2488
|
+
incoming.conversationType === "dm" &&
|
|
2489
|
+
!options?.resolveExecutionContext
|
|
2382
2490
|
) {
|
|
2383
2491
|
try {
|
|
2384
2492
|
defaultExecutionContext = await withCredentialContext(
|
|
@@ -2386,51 +2494,48 @@ export function createIntegrationsPlugin(
|
|
|
2386
2494
|
() => resolveDefaultIntegrationExecutionContext(incoming!),
|
|
2387
2495
|
);
|
|
2388
2496
|
} catch (err) {
|
|
2389
|
-
//
|
|
2390
|
-
//
|
|
2391
|
-
//
|
|
2392
|
-
//
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
|
|
2414
|
-
},
|
|
2415
|
-
)
|
|
2416
|
-
: Promise.resolve(),
|
|
2417
|
-
).catch((noticeErr) => {
|
|
2497
|
+
// The legacy owner-only resolver predates org-bound identities
|
|
2498
|
+
// and must not turn a rejected Slack DM into an authenticated
|
|
2499
|
+
// owner run. Custom resolveExecutionContext is checked above and
|
|
2500
|
+
// skips this default ladder entirely so apps can fully own auth
|
|
2501
|
+
// without framework membership checks or identity side effects.
|
|
2502
|
+
const declined =
|
|
2503
|
+
err instanceof IntegrationIdentityDeclinedError ? err : null;
|
|
2504
|
+
if (declined) {
|
|
2505
|
+
console.warn(
|
|
2506
|
+
`[integrations] default Slack DM identity declined message:`,
|
|
2507
|
+
declined.message,
|
|
2508
|
+
);
|
|
2509
|
+
if (adapter.sendSystemNotice) {
|
|
2510
|
+
try {
|
|
2511
|
+
await enqueueSystemNotice(
|
|
2512
|
+
event,
|
|
2513
|
+
incoming!,
|
|
2514
|
+
declined.userFacingMessage,
|
|
2515
|
+
{
|
|
2516
|
+
dedupeKey: `decline:${incoming!.tenantId ?? "unknown"}:${incoming!.senderId ?? "unknown"}:${declined.reason}`,
|
|
2517
|
+
dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
|
|
2518
|
+
},
|
|
2519
|
+
);
|
|
2520
|
+
} catch (noticeErr) {
|
|
2418
2521
|
console.warn(
|
|
2419
|
-
`[integrations] decline notice
|
|
2522
|
+
`[integrations] could not persist decline notice:`,
|
|
2420
2523
|
noticeErr instanceof Error
|
|
2421
2524
|
? noticeErr.message
|
|
2422
2525
|
: noticeErr,
|
|
2423
2526
|
);
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
`[integrations] default Slack DM identity denied message:`,
|
|
2428
|
-
err,
|
|
2429
|
-
);
|
|
2527
|
+
setResponseStatus(event, 500);
|
|
2528
|
+
return { error: "notice enqueue failed" };
|
|
2529
|
+
}
|
|
2430
2530
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2531
|
+
} else {
|
|
2532
|
+
console.error(
|
|
2533
|
+
`[integrations] default Slack DM identity denied message:`,
|
|
2534
|
+
err,
|
|
2535
|
+
);
|
|
2433
2536
|
}
|
|
2537
|
+
setResponseStatus(event, 200);
|
|
2538
|
+
return "ok";
|
|
2434
2539
|
}
|
|
2435
2540
|
}
|
|
2436
2541
|
let executionContext: IntegrationExecutionContext = {
|
|
@@ -2456,14 +2561,42 @@ export function createIntegrationsPlugin(
|
|
|
2456
2561
|
} else if (defaultExecutionContext) {
|
|
2457
2562
|
executionContext = defaultExecutionContext;
|
|
2458
2563
|
if (defaultExecutionContext.anonymousMember) {
|
|
2564
|
+
if (!options?.allowAnonymousOrgScopedSlackDm) {
|
|
2565
|
+
const senderEmail =
|
|
2566
|
+
typeof incoming.senderEmail === "string" &&
|
|
2567
|
+
incoming.senderEmail.trim()
|
|
2568
|
+
? incoming.senderEmail.trim()
|
|
2569
|
+
: null;
|
|
2570
|
+
const noticeText = senderEmail
|
|
2571
|
+
? `I couldn't match your Slack account to an organization member, so I can't run this request. Ask an organization admin to add ${senderEmail}, then try again.`
|
|
2572
|
+
: "I couldn't verify your Slack account email, so I can't run this request. Ask an organization admin to reconnect Slack with the users:read.email scope, then try again.";
|
|
2573
|
+
if (adapter.sendSystemNotice) {
|
|
2574
|
+
try {
|
|
2575
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2576
|
+
dedupeKey: `anonymous-tier-disabled:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2577
|
+
});
|
|
2578
|
+
} catch (noticeErr) {
|
|
2579
|
+
console.warn(
|
|
2580
|
+
`[integrations] could not persist unlinked-member notice:`,
|
|
2581
|
+
noticeErr instanceof Error
|
|
2582
|
+
? noticeErr.message
|
|
2583
|
+
: noticeErr,
|
|
2584
|
+
);
|
|
2585
|
+
setResponseStatus(event, 500);
|
|
2586
|
+
return { error: "notice enqueue failed" };
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
setResponseStatus(event, 200);
|
|
2590
|
+
return "ok";
|
|
2591
|
+
}
|
|
2459
2592
|
// The anonymous tier must never be silent. (1) The agent run
|
|
2460
2593
|
// can tell: the note rides the serialized `incoming` into the
|
|
2461
2594
|
// queued task and surfaces via <integration-context>.
|
|
2462
2595
|
incoming.identityNote =
|
|
2463
2596
|
"Caller is an unlinked Slack workspace member running with organization-wide visibility only; personal or privately-shared data is not accessible. They can get personal access by having an admin add their Slack email to the organization (or by reconnecting Slack with the users:read.email scope).";
|
|
2464
|
-
// (2) The sender gets a one-time heads-up
|
|
2465
|
-
//
|
|
2466
|
-
//
|
|
2597
|
+
// (2) The sender gets a one-time heads-up through the same
|
|
2598
|
+
// durable SQL queue as agent work. The self-dispatch is only a
|
|
2599
|
+
// latency optimization; the retry sweep guarantees delivery.
|
|
2467
2600
|
if (adapter.sendSystemNotice) {
|
|
2468
2601
|
const senderEmail =
|
|
2469
2602
|
typeof incoming.senderEmail === "string" &&
|
|
@@ -2473,16 +2606,18 @@ export function createIntegrationsPlugin(
|
|
|
2473
2606
|
const noticeText = senderEmail
|
|
2474
2607
|
? `Heads up: I couldn't match your Slack account to an organization member, so I can only use org-wide data. Ask an admin to add ${senderEmail} to the organization for personal access.`
|
|
2475
2608
|
: "Heads up: I couldn't verify your Slack account's email, so I can only use org-wide data. Ask an admin to update the Slack connection with the users:read.email scope for personal access.";
|
|
2476
|
-
|
|
2477
|
-
|
|
2609
|
+
try {
|
|
2610
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2478
2611
|
dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2479
|
-
})
|
|
2480
|
-
|
|
2612
|
+
});
|
|
2613
|
+
} catch (noticeErr) {
|
|
2481
2614
|
console.warn(
|
|
2482
|
-
`[integrations] anonymous-tier notice
|
|
2615
|
+
`[integrations] could not persist anonymous-tier notice:`,
|
|
2483
2616
|
noticeErr instanceof Error ? noticeErr.message : noticeErr,
|
|
2484
2617
|
);
|
|
2485
|
-
|
|
2618
|
+
setResponseStatus(event, 500);
|
|
2619
|
+
return { error: "notice enqueue failed" };
|
|
2620
|
+
}
|
|
2486
2621
|
}
|
|
2487
2622
|
}
|
|
2488
2623
|
} else if (options?.resolveOwner) {
|
|
@@ -438,6 +438,13 @@ export interface IntegrationsPluginOptions {
|
|
|
438
438
|
resolveExecutionContext?: (
|
|
439
439
|
incoming: IncomingMessage,
|
|
440
440
|
) => IntegrationExecutionContext | Promise<IntegrationExecutionContext>;
|
|
441
|
+
/**
|
|
442
|
+
* Explicitly allow an unlinked, verified Slack workspace member to run a DM
|
|
443
|
+
* with the installation organization's shared/service visibility. Disabled
|
|
444
|
+
* by default: DM identity resolution fails closed unless an app deliberately
|
|
445
|
+
* accepts this wider access tier.
|
|
446
|
+
*/
|
|
447
|
+
allowAnonymousOrgScopedSlackDm?: boolean;
|
|
441
448
|
/**
|
|
442
449
|
* Optional preprocessor for inbound platform messages. Can intercept special
|
|
443
450
|
* commands (such as `/link`) before the agent loop runs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-replay.d.ts","sourceRoot":"","sources":["../../src/client/session-replay.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,uBAAuB,GAC/B,MAAM,GACN,MAAM,GACN,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"session-replay.d.ts","sourceRoot":"","sources":["../../src/client/session-replay.ts"],"names":[],"mappings":"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"}
|
|
@@ -127,6 +127,7 @@ function getState() {
|
|
|
127
127
|
queue: [],
|
|
128
128
|
queuedBytes: 0,
|
|
129
129
|
retryBatches: [],
|
|
130
|
+
transientClientErrorFailures: 0,
|
|
130
131
|
flushTimer: null,
|
|
131
132
|
maxDurationTimer: null,
|
|
132
133
|
flushing: false,
|
|
@@ -149,6 +150,7 @@ function getState() {
|
|
|
149
150
|
const state = g[SESSION_REPLAY_STATE_KEY];
|
|
150
151
|
// Keep Vite HMR safe when an older recorder state survives a module reload.
|
|
151
152
|
state.resourceNodes ??= new Map();
|
|
153
|
+
state.transientClientErrorFailures ??= 0;
|
|
152
154
|
state.restoreIframeBridge ??= null;
|
|
153
155
|
state.pendingFlushReason ??= null;
|
|
154
156
|
state.pendingFlushWaiters ??= [];
|
|
@@ -658,14 +660,12 @@ const REPLAY_RESOURCE_TAGS = new Set([
|
|
|
658
660
|
"audio",
|
|
659
661
|
"track",
|
|
660
662
|
"input",
|
|
661
|
-
"object",
|
|
662
663
|
"link",
|
|
663
664
|
]);
|
|
664
665
|
const NO_REPLAY_RESOURCE_ATTRIBUTES = new Set();
|
|
665
666
|
const REPLAY_SRC_ATTRIBUTES = new Set(["src"]);
|
|
666
667
|
const REPLAY_SRCSET_ATTRIBUTES = new Set(["src", "srcset"]);
|
|
667
668
|
const REPLAY_VIDEO_ATTRIBUTES = new Set(["src", "poster"]);
|
|
668
|
-
const REPLAY_OBJECT_ATTRIBUTES = new Set(["data"]);
|
|
669
669
|
const REPLAY_HREF_ATTRIBUTES = new Set(["href"]);
|
|
670
670
|
function replayAttributeString(attributes, key) {
|
|
671
671
|
return typeof attributes[key] === "string"
|
|
@@ -696,8 +696,6 @@ function replayPreservedResourceAttributes(node) {
|
|
|
696
696
|
case "audio":
|
|
697
697
|
case "track":
|
|
698
698
|
return REPLAY_SRC_ATTRIBUTES;
|
|
699
|
-
case "object":
|
|
700
|
-
return REPLAY_OBJECT_ATTRIBUTES;
|
|
701
699
|
case "input":
|
|
702
700
|
return node.type === "image"
|
|
703
701
|
? REPLAY_SRC_ATTRIBUTES
|
|
@@ -991,11 +989,16 @@ class ReplayUploadHttpError extends Error {
|
|
|
991
989
|
}
|
|
992
990
|
/** 4xx statuses where retrying the exact same batch can never succeed.
|
|
993
991
|
* Keep this deliberately narrow: 401/403/404 can be temporary during auth or
|
|
994
|
-
* deploy transitions,
|
|
995
|
-
*
|
|
992
|
+
* deploy transitions, so they get a small retry budget before the rejected
|
|
993
|
+
* episode is stopped. The budget prevents a persistent configuration failure
|
|
994
|
+
* from pinning retryBatches while rrweb events grow without bound. */
|
|
996
995
|
function isDefinitiveReplayUploadClientError(status) {
|
|
997
996
|
return status === 400 || status === 409 || status === 413 || status === 422;
|
|
998
997
|
}
|
|
998
|
+
const MAX_TRANSIENT_REPLAY_CLIENT_FAILURES = 3;
|
|
999
|
+
function isTransientReplayUploadClientError(status) {
|
|
1000
|
+
return status === 401 || status === 403 || status === 404;
|
|
1001
|
+
}
|
|
999
1002
|
async function sendReplayUpload(options, body, callbacks = {}) {
|
|
1000
1003
|
if (isCrossOriginReplayEndpoint(options.endpoint)) {
|
|
1001
1004
|
const canUseKeepalive = canUseReplayKeepalive(body);
|
|
@@ -1229,6 +1232,7 @@ export async function flushSessionReplay(reason = "manual") {
|
|
|
1229
1232
|
if (!reservedSequence)
|
|
1230
1233
|
advanceReplaySequence(state, payload);
|
|
1231
1234
|
state.automaticConflictRestartAttempted = false;
|
|
1235
|
+
state.transientClientErrorFailures = 0;
|
|
1232
1236
|
uploaded = true;
|
|
1233
1237
|
}
|
|
1234
1238
|
catch (error) {
|
|
@@ -1241,8 +1245,20 @@ export async function flushSessionReplay(reason = "manual") {
|
|
|
1241
1245
|
const rejectedStatus = error instanceof ReplayUploadHttpError ? error.status : null;
|
|
1242
1246
|
const splitBatch = rejectedStatus === 413 ? splitReplayBatch(events) : null;
|
|
1243
1247
|
const isUnsplittableOversizedBatch = rejectedStatus === 413 && splitBatch === null;
|
|
1248
|
+
const isTransientClientError = rejectedStatus !== null &&
|
|
1249
|
+
isTransientReplayUploadClientError(rejectedStatus);
|
|
1250
|
+
if (isTransientClientError) {
|
|
1251
|
+
state.transientClientErrorFailures += 1;
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
state.transientClientErrorFailures = 0;
|
|
1255
|
+
}
|
|
1256
|
+
const exhaustedTransientClientRetries = isTransientClientError &&
|
|
1257
|
+
state.transientClientErrorFailures >=
|
|
1258
|
+
MAX_TRANSIENT_REPLAY_CLIENT_FAILURES;
|
|
1244
1259
|
const isDefinitiveClientError = error instanceof ReplayUploadHttpError &&
|
|
1245
|
-
isDefinitiveReplayUploadClientError(error.status)
|
|
1260
|
+
(isDefinitiveReplayUploadClientError(error.status) ||
|
|
1261
|
+
exhaustedTransientClientRetries) &&
|
|
1246
1262
|
!splitBatch &&
|
|
1247
1263
|
!isUnsplittableOversizedBatch;
|
|
1248
1264
|
if (splitBatch) {
|
|
@@ -2202,6 +2218,7 @@ export async function startSessionReplay(options = {}) {
|
|
|
2202
2218
|
// This is a new caller-initiated recording episode. A prior episode's
|
|
2203
2219
|
// conflict-loop guard must not prevent this one from recovering once.
|
|
2204
2220
|
state.automaticConflictRestartAttempted = false;
|
|
2221
|
+
state.transientClientErrorFailures = 0;
|
|
2205
2222
|
const startGeneration = ++state.startGeneration;
|
|
2206
2223
|
let startPromise;
|
|
2207
2224
|
startPromise = startSessionReplayRecorder(state, normalized, sessionId, sampled, initialProperties, startGeneration).finally(() => {
|