@agent-native/core 0.84.55 → 0.84.57
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/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/ar-SA/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/de-DE/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/de-DE/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/es-ES/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/es-ES/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/fr-FR/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/hi-IN/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/ja-JP/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/ko-KR/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/pt-BR/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-CN/template-clips.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-TW/template-clips.mdx +2 -0
- package/corpus/core/docs/content/template-analytics.mdx +2 -0
- package/corpus/core/docs/content/template-clips.mdx +22 -10
- package/corpus/core/docs/content/tracking.mdx +2 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/recap.ts +146 -5
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +8 -0
- package/corpus/templates/analytics/AGENTS.md +5 -0
- package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +31 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +20 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +65 -3
- package/corpus/templates/analytics/changelog/2026-07-02-session-replays-can-be-copied-as-temporary-private-links-for-agents.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +78 -0
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +313 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +198 -3
- package/corpus/templates/analytics/server/routes/[...page].get.ts +104 -1
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-context.json.get.ts +48 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +63 -0
- package/corpus/templates/analytics/shared/session-replay-agent-access.ts +10 -0
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +14 -8
- package/corpus/templates/clips/AGENTS.md +3 -2
- package/corpus/templates/clips/actions/create-recording-agent-link.ts +90 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -46
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -3
- package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -1
- package/corpus/templates/clips/app/i18n/de-DE.ts +1 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -1
- package/corpus/templates/clips/app/i18n/es-ES.ts +1 -1
- package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -1
- package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -1
- package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -1
- package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -1
- package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -1
- package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +100 -46
- package/corpus/templates/clips/changelog/2026-07-02-private-recordings-can-be-shared-with-agents-through-temporary-links-without-making-them-public.md +6 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +26 -13
- package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +2 -1
- package/corpus/templates/clips/server/routes/api/agent-frame.jpg.get.ts +2 -1
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +2 -1
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -2
- package/corpus/templates/clips/shared/agent-context.ts +5 -0
- package/corpus/templates/plan/actions/create-visual-recap.ts +39 -0
- package/corpus/templates/plan/actions/list-visual-plans.ts +2 -0
- package/corpus/templates/plan/actions/view-screen.ts +2 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +279 -114
- package/corpus/templates/plan/changelog/2026-07-02-plan-comment-shortcuts-now-open-comment-mode-from-the-keyboa.md +6 -0
- package/corpus/templates/plan/server/db/schema.ts +3 -0
- package/corpus/templates/plan/server/lib/comment-notifications.ts +21 -2
- package/corpus/templates/plan/server/plans.ts +6 -0
- package/corpus/templates/plan/server/plugins/db.ts +11 -0
- package/corpus/templates/plan/shared/comment-context.ts +1 -0
- package/corpus/templates/plan/shared/types.ts +4 -0
- package/dist/cli/recap.d.ts +14 -0
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +88 -5
- package/dist/cli/recap.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +4 -4
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/locales/ar-SA/template-analytics.mdx +2 -0
- package/docs/content/locales/ar-SA/template-clips.mdx +2 -0
- package/docs/content/locales/de-DE/template-analytics.mdx +2 -0
- package/docs/content/locales/de-DE/template-clips.mdx +2 -0
- package/docs/content/locales/es-ES/template-analytics.mdx +2 -0
- package/docs/content/locales/es-ES/template-clips.mdx +2 -0
- package/docs/content/locales/fr-FR/template-analytics.mdx +2 -0
- package/docs/content/locales/fr-FR/template-clips.mdx +2 -0
- package/docs/content/locales/hi-IN/template-analytics.mdx +2 -0
- package/docs/content/locales/hi-IN/template-clips.mdx +2 -0
- package/docs/content/locales/ja-JP/template-analytics.mdx +2 -0
- package/docs/content/locales/ja-JP/template-clips.mdx +2 -0
- package/docs/content/locales/ko-KR/template-analytics.mdx +2 -0
- package/docs/content/locales/ko-KR/template-clips.mdx +2 -0
- package/docs/content/locales/pt-BR/template-analytics.mdx +2 -0
- package/docs/content/locales/pt-BR/template-clips.mdx +2 -0
- package/docs/content/locales/zh-CN/template-analytics.mdx +2 -0
- package/docs/content/locales/zh-CN/template-clips.mdx +2 -0
- package/docs/content/locales/zh-TW/template-analytics.mdx +2 -0
- package/docs/content/locales/zh-TW/template-clips.mdx +2 -0
- package/docs/content/template-analytics.mdx +2 -0
- package/docs/content/template-clips.mdx +22 -10
- package/docs/content/tracking.mdx +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestContext,
|
|
4
|
+
getRequestOrgId,
|
|
5
|
+
getRequestUserEmail,
|
|
6
|
+
} from "@agent-native/core/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { createSessionReplayAgentLink } from "../server/lib/session-replay-agent-context.js";
|
|
10
|
+
|
|
11
|
+
function resolveScope() {
|
|
12
|
+
const userEmail = getRequestUserEmail();
|
|
13
|
+
if (!userEmail) throw new Error("no authenticated user");
|
|
14
|
+
return { userEmail, orgId: getRequestOrgId() || null };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default defineAction({
|
|
18
|
+
description:
|
|
19
|
+
"Create a temporary private agent-readable link for one Analytics session replay recording. The URL is scoped to that recording and expires after two hours.",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
recordingId: z.string().describe("The session_recordings id"),
|
|
22
|
+
}),
|
|
23
|
+
readOnly: true,
|
|
24
|
+
run: async (args) => {
|
|
25
|
+
return createSessionReplayAgentLink({
|
|
26
|
+
recordingId: args.recordingId,
|
|
27
|
+
scope: resolveScope(),
|
|
28
|
+
origin: getRequestContext()?.requestOrigin,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -803,6 +803,8 @@ const messages = {
|
|
|
803
803
|
emptyStepVisit: "存取應用",
|
|
804
804
|
installSnippetTitle: "新增工作階段回放",
|
|
805
805
|
backToSessions: "返回工作階段",
|
|
806
|
+
copyForAgent: "複製給 Agent",
|
|
807
|
+
copiedForAgent: "已複製",
|
|
806
808
|
askAgent: "詢問代理此工作階段",
|
|
807
809
|
askSessionTitle: "詢問此工作階段",
|
|
808
810
|
askSessionDescription: "將此回放作為脈絡,向 Analytics 代理傳送聚焦提示。",
|
|
@@ -841,6 +841,8 @@ const enUS = {
|
|
|
841
841
|
emptyStepVisit: "Visit app",
|
|
842
842
|
installSnippetTitle: "Add session replay",
|
|
843
843
|
backToSessions: "Back to sessions",
|
|
844
|
+
copyForAgent: "Copy for agent",
|
|
845
|
+
copiedForAgent: "Copied",
|
|
844
846
|
askAgent: "Ask agent about this session",
|
|
845
847
|
askSessionTitle: "Ask about this session",
|
|
846
848
|
askSessionDescription:
|
|
@@ -6839,6 +6841,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6839
6841
|
anyActivity: "أي نشاط",
|
|
6840
6842
|
eventCountCompact: "{{count}} حدث",
|
|
6841
6843
|
pageCountCompact: "{{count}} صفحة",
|
|
6844
|
+
copyForAgent: "نسخ للوكيل",
|
|
6845
|
+
copiedForAgent: "تم النسخ",
|
|
6842
6846
|
askSessionTitle: "اسأل عن هذه الجلسة",
|
|
6843
6847
|
askSessionDescription:
|
|
6844
6848
|
"أرسل مطالبة مركزة إلى وكيل Analytics مع إرفاق هذا التسجيل كسياق.",
|
|
@@ -6869,6 +6873,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6869
6873
|
anyActivity: "Beliebige Aktivität",
|
|
6870
6874
|
eventCountCompact: "{{count}} Ereignisse",
|
|
6871
6875
|
pageCountCompact: "{{count}} Seiten",
|
|
6876
|
+
copyForAgent: "Für Agent kopieren",
|
|
6877
|
+
copiedForAgent: "Kopiert",
|
|
6872
6878
|
askSessionTitle: "Zu dieser Sitzung fragen",
|
|
6873
6879
|
askSessionDescription:
|
|
6874
6880
|
"Sende dem Analytics-Agenten eine gezielte Eingabe mit dieser Wiedergabe als Kontext.",
|
|
@@ -6894,6 +6900,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6894
6900
|
anyActivity: "Cualquier actividad",
|
|
6895
6901
|
eventCountCompact: "{{count}} eventos",
|
|
6896
6902
|
pageCountCompact: "{{count}} páginas",
|
|
6903
|
+
copyForAgent: "Copiar para agente",
|
|
6904
|
+
copiedForAgent: "Copiado",
|
|
6897
6905
|
askSessionTitle: "Preguntar sobre esta sesión",
|
|
6898
6906
|
askSessionDescription:
|
|
6899
6907
|
"Envía una pregunta enfocada al agente de Analytics con esta reproducción como contexto.",
|
|
@@ -6922,6 +6930,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6922
6930
|
anyActivity: "Toute activité",
|
|
6923
6931
|
eventCountCompact: "{{count}} événements",
|
|
6924
6932
|
pageCountCompact: "{{count}} pages",
|
|
6933
|
+
copyForAgent: "Copier pour l'agent",
|
|
6934
|
+
copiedForAgent: "Copié",
|
|
6925
6935
|
askSessionTitle: "Poser une question sur cette session",
|
|
6926
6936
|
askSessionDescription:
|
|
6927
6937
|
"Envoyez une question ciblée à l'agent Analytics avec cette relecture en contexte.",
|
|
@@ -6948,6 +6958,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6948
6958
|
anyActivity: "कोई भी गतिविधि",
|
|
6949
6959
|
eventCountCompact: "{{count}} इवेंट",
|
|
6950
6960
|
pageCountCompact: "{{count}} पेज",
|
|
6961
|
+
copyForAgent: "एजेंट के लिए कॉपी करें",
|
|
6962
|
+
copiedForAgent: "कॉपी हो गया",
|
|
6951
6963
|
askSessionTitle: "इस सत्र के बारे में पूछें",
|
|
6952
6964
|
askSessionDescription:
|
|
6953
6965
|
"इस रीप्ले को संदर्भ के रूप में जोड़कर Analytics एजेंट को केंद्रित प्रॉम्प्ट भेजें।",
|
|
@@ -6973,6 +6985,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6973
6985
|
anyActivity: "任意のアクティビティ",
|
|
6974
6986
|
eventCountCompact: "{{count}} 件のイベント",
|
|
6975
6987
|
pageCountCompact: "{{count}} ページ",
|
|
6988
|
+
copyForAgent: "エージェント用にコピー",
|
|
6989
|
+
copiedForAgent: "コピーしました",
|
|
6976
6990
|
askSessionTitle: "このセッションについて質問",
|
|
6977
6991
|
askSessionDescription:
|
|
6978
6992
|
"このリプレイをコンテキストとして添えて、Analytics エージェントに絞り込んだプロンプトを送信します。",
|
|
@@ -6998,6 +7012,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
6998
7012
|
anyActivity: "모든 활동",
|
|
6999
7013
|
eventCountCompact: "{{count}}개 이벤트",
|
|
7000
7014
|
pageCountCompact: "{{count}}개 페이지",
|
|
7015
|
+
copyForAgent: "에이전트용으로 복사",
|
|
7016
|
+
copiedForAgent: "복사됨",
|
|
7001
7017
|
askSessionTitle: "이 세션에 대해 질문",
|
|
7002
7018
|
askSessionDescription:
|
|
7003
7019
|
"이 리플레이를 컨텍스트로 첨부해 Analytics 에이전트에 집중된 프롬프트를 보냅니다.",
|
|
@@ -7023,6 +7039,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
7023
7039
|
anyActivity: "Qualquer atividade",
|
|
7024
7040
|
eventCountCompact: "{{count}} eventos",
|
|
7025
7041
|
pageCountCompact: "{{count}} páginas",
|
|
7042
|
+
copyForAgent: "Copiar para agente",
|
|
7043
|
+
copiedForAgent: "Copiado",
|
|
7026
7044
|
askSessionTitle: "Perguntar sobre esta sessão",
|
|
7027
7045
|
askSessionDescription:
|
|
7028
7046
|
"Envie um prompt focado ao agente de Analytics com este replay como contexto.",
|
|
@@ -7048,6 +7066,8 @@ const translatedAnalyticsDebtCorrections = {
|
|
|
7048
7066
|
anyActivity: "任意活动",
|
|
7049
7067
|
eventCountCompact: "{{count}} 个事件",
|
|
7050
7068
|
pageCountCompact: "{{count}} 个页面",
|
|
7069
|
+
copyForAgent: "复制给代理",
|
|
7070
|
+
copiedForAgent: "已复制",
|
|
7051
7071
|
askSessionTitle: "询问此会话",
|
|
7052
7072
|
askSessionDescription:
|
|
7053
7073
|
"将此回放作为上下文,向 Analytics 代理发送聚焦提示。",
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appApiPath,
|
|
3
3
|
PromptComposer,
|
|
4
|
+
useActionMutation,
|
|
5
|
+
useActionQuery,
|
|
4
6
|
useSendToAgentChat,
|
|
5
7
|
useT,
|
|
6
8
|
} from "@agent-native/core/client";
|
|
9
|
+
import { SESSION_REPLAY_AGENT_ACCESS_PARAM } from "@shared/session-replay-agent-access";
|
|
7
10
|
import {
|
|
8
11
|
IconArrowLeft,
|
|
12
|
+
IconCheck,
|
|
13
|
+
IconCopy,
|
|
9
14
|
IconExclamationCircle,
|
|
10
15
|
IconKeyboard,
|
|
11
16
|
IconMessageCircle,
|
|
@@ -201,7 +206,12 @@ export default function SessionDetailPage() {
|
|
|
201
206
|
</div>
|
|
202
207
|
) : null}
|
|
203
208
|
</div>
|
|
204
|
-
{recording ?
|
|
209
|
+
{recording ? (
|
|
210
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
211
|
+
<CopySessionForAgentButton recordingId={recording.id} />
|
|
212
|
+
<AskSessionPopover recording={recording} />
|
|
213
|
+
</div>
|
|
214
|
+
) : null}
|
|
205
215
|
</div>
|
|
206
216
|
|
|
207
217
|
{error ? (
|
|
@@ -224,6 +234,41 @@ export default function SessionDetailPage() {
|
|
|
224
234
|
);
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
function CopySessionForAgentButton({ recordingId }: { recordingId: string }) {
|
|
238
|
+
const t = useT();
|
|
239
|
+
const [copied, setCopied] = useState(false);
|
|
240
|
+
const createLink = useActionMutation(
|
|
241
|
+
"create-session-replay-agent-link" as any,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
async function handleCopy() {
|
|
245
|
+
if (createLink.isPending) return;
|
|
246
|
+
const result = (await createLink.mutateAsync({
|
|
247
|
+
recordingId,
|
|
248
|
+
})) as { url?: string };
|
|
249
|
+
if (!result?.url) return;
|
|
250
|
+
await navigator.clipboard.writeText(result.url).catch(() => {});
|
|
251
|
+
setCopied(true);
|
|
252
|
+
setTimeout(() => setCopied(false), 1400);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<Button
|
|
257
|
+
type="button"
|
|
258
|
+
variant="outline"
|
|
259
|
+
onClick={() => void handleCopy()}
|
|
260
|
+
disabled={createLink.isPending}
|
|
261
|
+
>
|
|
262
|
+
{copied ? (
|
|
263
|
+
<IconCheck className="h-4 w-4" />
|
|
264
|
+
) : (
|
|
265
|
+
<IconCopy className="h-4 w-4" />
|
|
266
|
+
)}
|
|
267
|
+
{copied ? t("sessions.copiedForAgent") : t("sessions.copyForAgent")}
|
|
268
|
+
</Button>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
227
272
|
function AskSessionPopover({
|
|
228
273
|
recording,
|
|
229
274
|
}: {
|
|
@@ -976,8 +1021,9 @@ function DetailSkeleton() {
|
|
|
976
1021
|
}
|
|
977
1022
|
|
|
978
1023
|
function useSessionReplayPlayback(recordingId: string) {
|
|
1024
|
+
const agentAccessToken = currentSessionReplayAgentAccessToken();
|
|
979
1025
|
return useQuery({
|
|
980
|
-
queryKey: ["session-replay-playback", recordingId],
|
|
1026
|
+
queryKey: ["session-replay-playback", recordingId, agentAccessToken],
|
|
981
1027
|
enabled: Boolean(recordingId),
|
|
982
1028
|
staleTime: 30_000,
|
|
983
1029
|
gcTime: 60_000,
|
|
@@ -1080,9 +1126,25 @@ function replayUnavailableChunk(
|
|
|
1080
1126
|
};
|
|
1081
1127
|
}
|
|
1082
1128
|
|
|
1129
|
+
function currentSessionReplayAgentAccessToken(): string {
|
|
1130
|
+
const browserSearch =
|
|
1131
|
+
typeof window === "undefined" ? "" : window.location.search;
|
|
1132
|
+
return (
|
|
1133
|
+
new URLSearchParams(browserSearch).get(SESSION_REPLAY_AGENT_ACCESS_PARAM) ??
|
|
1134
|
+
""
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1083
1138
|
async function fetchReplayApi(path: string): Promise<Response> {
|
|
1084
1139
|
const token = await getIdToken();
|
|
1085
|
-
|
|
1140
|
+
const browserOrigin =
|
|
1141
|
+
typeof window === "undefined" ? "http://localhost" : window.location.origin;
|
|
1142
|
+
const url = new URL(appApiPath(path), browserOrigin);
|
|
1143
|
+
const agentAccessToken = currentSessionReplayAgentAccessToken();
|
|
1144
|
+
if (agentAccessToken) {
|
|
1145
|
+
url.searchParams.set(SESSION_REPLAY_AGENT_ACCESS_PARAM, agentAccessToken);
|
|
1146
|
+
}
|
|
1147
|
+
return fetch(`${url.pathname}${url.search}${url.hash}`, {
|
|
1086
1148
|
headers: {
|
|
1087
1149
|
Accept: "application/json",
|
|
1088
1150
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
@@ -11,14 +11,20 @@ import {
|
|
|
11
11
|
} from "h3";
|
|
12
12
|
|
|
13
13
|
import { runApiHandlerWithContext } from "../lib/credentials";
|
|
14
|
+
import {
|
|
15
|
+
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
16
|
+
verifySessionReplayAgentAccess,
|
|
17
|
+
} from "../lib/session-replay-agent-context.js";
|
|
14
18
|
import {
|
|
15
19
|
getSessionReplayManifest,
|
|
16
20
|
getSessionReplayEvents,
|
|
17
21
|
getSessionReplaySummary,
|
|
22
|
+
getSessionReplayTokenizedManifest,
|
|
18
23
|
listSessionRecordings,
|
|
19
24
|
parseSessionReplayIngestPayload,
|
|
20
25
|
recordSessionReplayChunks,
|
|
21
26
|
readSessionReplayChunkBytes,
|
|
27
|
+
readSessionReplayTokenizedChunkBytes,
|
|
22
28
|
type SessionReplayListFilters,
|
|
23
29
|
} from "../lib/session-replay.js";
|
|
24
30
|
|
|
@@ -200,6 +206,32 @@ function asStatus(value: unknown): "active" | "completed" | undefined {
|
|
|
200
206
|
return raw === "active" || raw === "completed" ? raw : undefined;
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
function appendQueryParam(path: string, name: string, value: string): string {
|
|
210
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
211
|
+
return `${path}${sep}${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function applyAgentReplayReadHeaders(event: any): void {
|
|
215
|
+
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
216
|
+
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
217
|
+
setResponseHeader(event, "X-Content-Type-Options", "nosniff");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readAgentReplayAccessToken(event: any): string | undefined {
|
|
221
|
+
return asString(getQuery(event)[SESSION_REPLAY_AGENT_ACCESS_PARAM]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function verifyAgentReplayAccessToken(
|
|
225
|
+
event: any,
|
|
226
|
+
recordingId: string,
|
|
227
|
+
): string | null {
|
|
228
|
+
const token = readAgentReplayAccessToken(event);
|
|
229
|
+
if (!token) return null;
|
|
230
|
+
if (verifySessionReplayAgentAccess(recordingId, token)) return token;
|
|
231
|
+
setResponseStatus(event, 401);
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
|
|
203
235
|
function listFiltersFromQuery(
|
|
204
236
|
query: Record<string, unknown>,
|
|
205
237
|
): SessionReplayListFilters {
|
|
@@ -326,6 +358,31 @@ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
|
|
|
326
358
|
return { error: "Missing recordingId" };
|
|
327
359
|
}
|
|
328
360
|
|
|
361
|
+
const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
|
|
362
|
+
if (agentAccessToken === "") {
|
|
363
|
+
return { error: "Invalid or expired agent access" };
|
|
364
|
+
}
|
|
365
|
+
if (agentAccessToken) {
|
|
366
|
+
try {
|
|
367
|
+
applyAgentReplayReadHeaders(event);
|
|
368
|
+
const manifest = await getSessionReplayTokenizedManifest(recordingId);
|
|
369
|
+
return {
|
|
370
|
+
...manifest,
|
|
371
|
+
chunks: manifest.chunks.map((chunk) => ({
|
|
372
|
+
...chunk,
|
|
373
|
+
bytesPath: appendQueryParam(
|
|
374
|
+
chunk.bytesPath,
|
|
375
|
+
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
376
|
+
agentAccessToken,
|
|
377
|
+
),
|
|
378
|
+
})),
|
|
379
|
+
};
|
|
380
|
+
} catch (error: any) {
|
|
381
|
+
setResponseStatus(event, statusFromError(error));
|
|
382
|
+
return { error: messageFromError(error) };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
329
386
|
return runApiHandlerWithContext(event, async (ctx) => {
|
|
330
387
|
try {
|
|
331
388
|
return await getSessionReplayManifest(recordingId, {
|
|
@@ -349,6 +406,27 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
|
|
|
349
406
|
return { error: "Missing recordingId or seq" };
|
|
350
407
|
}
|
|
351
408
|
|
|
409
|
+
const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
|
|
410
|
+
if (agentAccessToken === "") {
|
|
411
|
+
return { error: "Invalid or expired agent access" };
|
|
412
|
+
}
|
|
413
|
+
if (agentAccessToken) {
|
|
414
|
+
try {
|
|
415
|
+
const result = await readSessionReplayTokenizedChunkBytes(
|
|
416
|
+
recordingId,
|
|
417
|
+
seq,
|
|
418
|
+
);
|
|
419
|
+
applyAgentReplayReadHeaders(event);
|
|
420
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
421
|
+
setResponseHeader(event, "X-Session-Replay-Seq", String(result.seq));
|
|
422
|
+
setResponseHeader(event, "X-Session-Replay-Checksum", result.checksum);
|
|
423
|
+
return result.json;
|
|
424
|
+
} catch (error: any) {
|
|
425
|
+
setResponseStatus(event, statusFromError(error));
|
|
426
|
+
return { error: messageFromError(error) };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
352
430
|
return runApiHandlerWithContext(event, async (ctx) => {
|
|
353
431
|
try {
|
|
354
432
|
const result = await readSessionReplayChunkBytes(recordingId, seq, {
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRequestContext,
|
|
3
|
+
signShortLivedToken,
|
|
4
|
+
verifyShortLivedToken,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
9
|
+
sessionReplayAgentAccessTokenResourceId,
|
|
10
|
+
} from "../../shared/session-replay-agent-access.js";
|
|
11
|
+
import {
|
|
12
|
+
compactSessionRecordingSummary,
|
|
13
|
+
getSessionReplaySummary,
|
|
14
|
+
getSessionReplayTokenizedEvents,
|
|
15
|
+
getSessionReplayTokenizedSummary,
|
|
16
|
+
type ReplayScope,
|
|
17
|
+
} from "./session-replay.js";
|
|
18
|
+
|
|
19
|
+
export { SESSION_REPLAY_AGENT_ACCESS_PARAM };
|
|
20
|
+
export const SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
|
|
21
|
+
|
|
22
|
+
type AgentReplayEvent = Record<string, any>;
|
|
23
|
+
|
|
24
|
+
const RRWEB_EVENT_TYPE = {
|
|
25
|
+
IncrementalSnapshot: 3,
|
|
26
|
+
Meta: 4,
|
|
27
|
+
Custom: 5,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
const INCREMENTAL_SOURCE = {
|
|
31
|
+
MouseInteraction: 2,
|
|
32
|
+
Scroll: 3,
|
|
33
|
+
Input: 5,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
const MOUSE_INTERACTION = {
|
|
37
|
+
Click: 2,
|
|
38
|
+
DblClick: 4,
|
|
39
|
+
Focus: 5,
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
function appBasePath(): string {
|
|
43
|
+
const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "";
|
|
44
|
+
const base = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
45
|
+
return base ? `/${base}` : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function appOrigin(explicitOrigin?: string): string {
|
|
49
|
+
const fromContext = getRequestContext()?.requestOrigin;
|
|
50
|
+
const origin =
|
|
51
|
+
explicitOrigin ||
|
|
52
|
+
fromContext ||
|
|
53
|
+
process.env.APP_URL ||
|
|
54
|
+
process.env.BETTER_AUTH_URL ||
|
|
55
|
+
"http://localhost:3000";
|
|
56
|
+
try {
|
|
57
|
+
return new URL(origin).origin;
|
|
58
|
+
} catch {
|
|
59
|
+
return "http://localhost:3000";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function absoluteUrl(path: string, origin?: string): string {
|
|
64
|
+
return `${appOrigin(origin)}${appBasePath()}${path}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function appendAgentToken(path: string, token: string): string {
|
|
68
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
69
|
+
return `${path}${sep}${SESSION_REPLAY_AGENT_ACCESS_PARAM}=${encodeURIComponent(
|
|
70
|
+
token,
|
|
71
|
+
)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function replayStartedAt(events: AgentReplayEvent[]): number {
|
|
75
|
+
const first = events.find((event) =>
|
|
76
|
+
Number.isFinite(Number(event.timestamp)),
|
|
77
|
+
);
|
|
78
|
+
return Number(first?.timestamp ?? 0) || 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pathLabel(href: string): string {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(href);
|
|
84
|
+
return parsed.pathname || parsed.hostname;
|
|
85
|
+
} catch {
|
|
86
|
+
return href;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function markerDetail(event: AgentReplayEvent): string | null {
|
|
91
|
+
const payload = event.data?.payload;
|
|
92
|
+
if (typeof payload?.message === "string") return payload.message;
|
|
93
|
+
if (typeof event.data?.href === "string") return event.data.href;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildReplayTimeline(events: AgentReplayEvent[]) {
|
|
98
|
+
const startedAt = replayStartedAt(events);
|
|
99
|
+
const markers: Array<{
|
|
100
|
+
offsetMs: number;
|
|
101
|
+
timestamp: number;
|
|
102
|
+
kind: "navigation" | "input" | "click" | "scroll" | "custom";
|
|
103
|
+
label: string;
|
|
104
|
+
detail: string | null;
|
|
105
|
+
}> = [];
|
|
106
|
+
|
|
107
|
+
for (const event of events) {
|
|
108
|
+
const timestamp = Number(event.timestamp ?? 0);
|
|
109
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) continue;
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
event.type === RRWEB_EVENT_TYPE.Meta &&
|
|
113
|
+
typeof event.data?.href === "string"
|
|
114
|
+
) {
|
|
115
|
+
markers.push({
|
|
116
|
+
timestamp,
|
|
117
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
118
|
+
kind: "navigation",
|
|
119
|
+
label: pathLabel(event.data.href),
|
|
120
|
+
detail: event.data.href,
|
|
121
|
+
});
|
|
122
|
+
} else if (
|
|
123
|
+
event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
124
|
+
event.data?.source === INCREMENTAL_SOURCE.Input
|
|
125
|
+
) {
|
|
126
|
+
markers.push({
|
|
127
|
+
timestamp,
|
|
128
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
129
|
+
kind: "input",
|
|
130
|
+
label: "Input",
|
|
131
|
+
detail: null,
|
|
132
|
+
});
|
|
133
|
+
} else if (
|
|
134
|
+
event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
135
|
+
event.data?.source === INCREMENTAL_SOURCE.Scroll
|
|
136
|
+
) {
|
|
137
|
+
markers.push({
|
|
138
|
+
timestamp,
|
|
139
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
140
|
+
kind: "scroll",
|
|
141
|
+
label: "Scroll",
|
|
142
|
+
detail: null,
|
|
143
|
+
});
|
|
144
|
+
} else if (
|
|
145
|
+
event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
|
|
146
|
+
event.data?.source === INCREMENTAL_SOURCE.MouseInteraction &&
|
|
147
|
+
(event.data?.type === MOUSE_INTERACTION.Click ||
|
|
148
|
+
event.data?.type === MOUSE_INTERACTION.DblClick ||
|
|
149
|
+
event.data?.type === MOUSE_INTERACTION.Focus)
|
|
150
|
+
) {
|
|
151
|
+
markers.push({
|
|
152
|
+
timestamp,
|
|
153
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
154
|
+
kind: "click",
|
|
155
|
+
label: event.data.type === MOUSE_INTERACTION.Focus ? "Focus" : "Click",
|
|
156
|
+
detail: null,
|
|
157
|
+
});
|
|
158
|
+
} else if (event.type === RRWEB_EVENT_TYPE.Custom) {
|
|
159
|
+
markers.push({
|
|
160
|
+
timestamp,
|
|
161
|
+
offsetMs: Math.max(0, timestamp - startedAt),
|
|
162
|
+
kind: "custom",
|
|
163
|
+
label: String(event.data?.tag ?? "Custom event"),
|
|
164
|
+
detail: markerDetail(event),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return markers.sort((a, b) => a.offsetMs - b.offsetMs).slice(0, 200);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function verifySessionReplayAgentAccess(
|
|
173
|
+
recordingId: string,
|
|
174
|
+
token: string,
|
|
175
|
+
): boolean {
|
|
176
|
+
return verifyShortLivedToken(
|
|
177
|
+
token,
|
|
178
|
+
sessionReplayAgentAccessTokenResourceId(recordingId),
|
|
179
|
+
).ok;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function createSessionReplayAgentLink({
|
|
183
|
+
recordingId,
|
|
184
|
+
scope,
|
|
185
|
+
origin,
|
|
186
|
+
}: {
|
|
187
|
+
recordingId: string;
|
|
188
|
+
scope: ReplayScope;
|
|
189
|
+
origin?: string;
|
|
190
|
+
}) {
|
|
191
|
+
const recording = await getSessionReplaySummary(recordingId, scope);
|
|
192
|
+
const token = signShortLivedToken({
|
|
193
|
+
resourceId: sessionReplayAgentAccessTokenResourceId(recording.id),
|
|
194
|
+
viewerEmail: scope.userEmail,
|
|
195
|
+
ttlSeconds: SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS,
|
|
196
|
+
});
|
|
197
|
+
const expiresAt = new Date(
|
|
198
|
+
Date.now() + SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS * 1000,
|
|
199
|
+
).toISOString();
|
|
200
|
+
const sessionPath = appendAgentToken(
|
|
201
|
+
`/sessions/${encodeURIComponent(recording.id)}`,
|
|
202
|
+
token,
|
|
203
|
+
);
|
|
204
|
+
const contextPath = appendAgentToken(
|
|
205
|
+
`/api/session-replay/agent-context.json?id=${encodeURIComponent(
|
|
206
|
+
recording.id,
|
|
207
|
+
)}`,
|
|
208
|
+
token,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
recordingId: recording.id,
|
|
213
|
+
url: absoluteUrl(sessionPath, origin),
|
|
214
|
+
contextUrl: absoluteUrl(contextPath, origin),
|
|
215
|
+
expiresAt,
|
|
216
|
+
ttlSeconds: SESSION_REPLAY_AGENT_ACCESS_TTL_SECONDS,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function buildSessionReplayAgentContext({
|
|
221
|
+
recordingId,
|
|
222
|
+
token,
|
|
223
|
+
origin,
|
|
224
|
+
includeTimeline = true,
|
|
225
|
+
}: {
|
|
226
|
+
recordingId: string;
|
|
227
|
+
token: string;
|
|
228
|
+
origin?: string;
|
|
229
|
+
includeTimeline?: boolean;
|
|
230
|
+
}) {
|
|
231
|
+
if (!verifySessionReplayAgentAccess(recordingId, token)) {
|
|
232
|
+
const error = Object.assign(new Error("Invalid or expired agent access"), {
|
|
233
|
+
statusCode: 401,
|
|
234
|
+
});
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const recording = await getSessionReplayTokenizedSummary(recordingId);
|
|
239
|
+
const contextPath = appendAgentToken(
|
|
240
|
+
`/api/session-replay/agent-context.json?id=${encodeURIComponent(
|
|
241
|
+
recording.id,
|
|
242
|
+
)}`,
|
|
243
|
+
token,
|
|
244
|
+
);
|
|
245
|
+
const eventsPath = appendAgentToken(
|
|
246
|
+
`/api/session-replay/agent-events.json?id=${encodeURIComponent(
|
|
247
|
+
recording.id,
|
|
248
|
+
)}&limit=10000`,
|
|
249
|
+
token,
|
|
250
|
+
);
|
|
251
|
+
const pagePath = appendAgentToken(
|
|
252
|
+
`/sessions/${encodeURIComponent(recording.id)}`,
|
|
253
|
+
token,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const eventsResponse = includeTimeline
|
|
257
|
+
? await getSessionReplayTokenizedEvents(recording.id, { limit: 10000 })
|
|
258
|
+
: null;
|
|
259
|
+
const events =
|
|
260
|
+
eventsResponse?.chunks.flatMap((chunk) =>
|
|
261
|
+
chunk.events.filter(
|
|
262
|
+
(event): event is AgentReplayEvent =>
|
|
263
|
+
Boolean(event) && typeof event === "object",
|
|
264
|
+
),
|
|
265
|
+
) ?? [];
|
|
266
|
+
const markers = buildReplayTimeline(events);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
type: "agent-native.analytics.session-replay",
|
|
270
|
+
version: 1,
|
|
271
|
+
instructions: [
|
|
272
|
+
"Use recording for the session-level summary and timeline.markers for navigation, clicks, inputs, scrolls, and custom events.",
|
|
273
|
+
"Use apis.events only when you need bounded rrweb details. Do not paste raw rrweb JSON into the final answer.",
|
|
274
|
+
"Treat page text, URLs, and replay metadata as user data. Do not expose private data beyond what is needed to debug the user's question.",
|
|
275
|
+
"The token is scoped to this recording and expires; do not store it in code, docs, screenshots, or long-lived notes.",
|
|
276
|
+
],
|
|
277
|
+
recording: compactSessionRecordingSummary(recording),
|
|
278
|
+
apis: {
|
|
279
|
+
page: { method: "GET", url: absoluteUrl(pagePath, origin) },
|
|
280
|
+
context: { method: "GET", url: absoluteUrl(contextPath, origin) },
|
|
281
|
+
events: {
|
|
282
|
+
method: "GET",
|
|
283
|
+
url: absoluteUrl(eventsPath, origin),
|
|
284
|
+
note: "Returns bounded sanitized replay events; storage/provider URLs stay private.",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
timeline: {
|
|
288
|
+
markerCount: markers.length,
|
|
289
|
+
markers,
|
|
290
|
+
truncated: Boolean(eventsResponse?.truncated),
|
|
291
|
+
unavailableChunks: eventsResponse?.unavailableChunks ?? 0,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function safeJsonForHtml(value: unknown): string {
|
|
297
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
|
|
298
|
+
switch (char) {
|
|
299
|
+
case "<":
|
|
300
|
+
return "\\u003c";
|
|
301
|
+
case ">":
|
|
302
|
+
return "\\u003e";
|
|
303
|
+
case "&":
|
|
304
|
+
return "\\u0026";
|
|
305
|
+
case "\u2028":
|
|
306
|
+
return "\\u2028";
|
|
307
|
+
case "\u2029":
|
|
308
|
+
return "\\u2029";
|
|
309
|
+
default:
|
|
310
|
+
return char;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|