@agent-native/core 0.79.20 → 0.79.22
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 +2 -2
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/docs/content/deployment.md +133 -0
- package/corpus/core/docs/content/faq.md +1 -1
- package/corpus/core/docs/content/multi-app-workspace.md +2 -0
- package/corpus/core/docs/content/tracking.md +3 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
- package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
- package/corpus/core/src/agent/engine/registry.ts +59 -11
- package/corpus/core/src/client/FeedbackButton.tsx +11 -1
- package/corpus/core/src/client/agent-engine-key.ts +57 -10
- package/corpus/core/src/client/analytics.ts +78 -27
- package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/session-replay.ts +159 -17
- package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
- package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
- package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
- package/corpus/core/src/server/core-routes-plugin.ts +26 -2
- package/corpus/core/src/styles/blocks.css +23 -7
- package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
- package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +5 -10
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
- package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
- package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
- package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +20 -4
- package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -2
- package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-27-desktop-storage-setup-flow.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
- package/corpus/templates/clips/desktop/src/app.tsx +32 -3
- package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
- package/corpus/templates/clips/desktop/src/main.tsx +2 -0
- package/corpus/templates/clips/desktop/src/styles.css +67 -0
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
- package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
- package/corpus/templates/clips/desktop/vite.config.ts +66 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
- package/corpus/templates/forms/actions/export-responses.ts +3 -2
- package/corpus/templates/forms/actions/list-responses.ts +2 -1
- package/corpus/templates/forms/actions/response-insights.ts +13 -4
- package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
- package/corpus/templates/forms/server/lib/integrations.ts +9 -6
- package/corpus/templates/forms/shared/submitter-email.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +1 -1
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
- package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
- package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
- package/dist/agent/engine/provider-env-vars.d.ts +1 -0
- package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
- package/dist/agent/engine/provider-env-vars.js +1 -0
- package/dist/agent/engine/provider-env-vars.js.map +1 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +39 -12
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/client/FeedbackButton.d.ts.map +1 -1
- package/dist/client/FeedbackButton.js +6 -1
- package/dist/client/FeedbackButton.js.map +1 -1
- package/dist/client/agent-engine-key.d.ts +14 -0
- package/dist/client/agent-engine-key.d.ts.map +1 -1
- package/dist/client/agent-engine-key.js +35 -10
- package/dist/client/agent-engine-key.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +71 -24
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
- package/dist/client/blocks/library/HighlightedCode.js +1 -1
- package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +115 -17
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +42 -6
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
- package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
- package/dist/server/agent-engine-api-key-route.js +71 -11
- package/dist/server/agent-engine-api-key-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/styles/blocks.css +23 -7
- package/docs/content/deployment.md +133 -0
- package/docs/content/faq.md +1 -1
- package/docs/content/multi-app-workspace.md +2 -0
- package/docs/content/tracking.md +3 -3
- package/package.json +1 -1
|
@@ -78,6 +78,10 @@ let _trackingIdentity: TrackingIdentity | null = null;
|
|
|
78
78
|
let _trackingIdentityResolved = false;
|
|
79
79
|
let _trackingSessionRefresh: Promise<void> | null = null;
|
|
80
80
|
let _trackingSessionRefreshInstalled = false;
|
|
81
|
+
let _sessionReplayOptions: SessionReplayOptions | null = null;
|
|
82
|
+
let _sessionReplayIdentitySnapshot: TrackingIdentity | null = null;
|
|
83
|
+
let _sessionReplayStartPromise: Promise<SessionReplayStartResult | null> | null =
|
|
84
|
+
null;
|
|
81
85
|
// Buffer for setSentryUser calls made before Sentry has initialized.
|
|
82
86
|
// `undefined` means "no pending update"; `null` means "pending clear".
|
|
83
87
|
let _pendingSentryUser: SentryUser | null | undefined = undefined;
|
|
@@ -264,17 +268,44 @@ function readTrackingString(value: unknown): string | undefined {
|
|
|
264
268
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
265
269
|
}
|
|
266
270
|
|
|
271
|
+
function stopSessionReplayForAuthClear(
|
|
272
|
+
previousIdentity: TrackingIdentity | null,
|
|
273
|
+
): void {
|
|
274
|
+
if (!_sessionReplayOptions?.requireSignedInUser) {
|
|
275
|
+
_sessionReplayIdentitySnapshot = null;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!previousIdentity?.userEmail) return;
|
|
279
|
+
_sessionReplayIdentitySnapshot = previousIdentity;
|
|
280
|
+
void import("./session-replay.js")
|
|
281
|
+
.then((mod) => mod.stopSessionReplay("auth-cleared"))
|
|
282
|
+
.catch(() => {
|
|
283
|
+
// Auth clearing should never fail because replay cleanup failed.
|
|
284
|
+
})
|
|
285
|
+
.finally(() => {
|
|
286
|
+
if (_sessionReplayIdentitySnapshot === previousIdentity) {
|
|
287
|
+
_sessionReplayIdentitySnapshot = null;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function clearTrackingIdentity(): void {
|
|
293
|
+
const previousIdentity = _trackingIdentity;
|
|
294
|
+
stopSessionReplayForAuthClear(previousIdentity);
|
|
295
|
+
_trackingIdentity = null;
|
|
296
|
+
}
|
|
297
|
+
|
|
267
298
|
function setTrackingIdentityFromSession(data: unknown): void {
|
|
268
299
|
const session = data as Record<string, unknown> | null;
|
|
269
300
|
if (!session || typeof session !== "object" || session.error) {
|
|
270
|
-
|
|
301
|
+
clearTrackingIdentity();
|
|
271
302
|
return;
|
|
272
303
|
}
|
|
273
304
|
const email = readTrackingString(session.email);
|
|
274
305
|
const authUserId = readTrackingString(session.userId);
|
|
275
306
|
const userId = email || authUserId;
|
|
276
307
|
if (!userId) {
|
|
277
|
-
|
|
308
|
+
clearTrackingIdentity();
|
|
278
309
|
return;
|
|
279
310
|
}
|
|
280
311
|
const userName = readTrackingString(session.name);
|
|
@@ -300,7 +331,7 @@ function refreshTrackingAuthSession(): Promise<void> {
|
|
|
300
331
|
setTrackingIdentityFromSession(data);
|
|
301
332
|
})
|
|
302
333
|
.catch(() => {
|
|
303
|
-
|
|
334
|
+
clearTrackingIdentity();
|
|
304
335
|
})
|
|
305
336
|
.finally(() => {
|
|
306
337
|
_trackingIdentityResolved = true;
|
|
@@ -320,8 +351,8 @@ function installTrackingAuthSessionRefresh(): void {
|
|
|
320
351
|
|
|
321
352
|
function applyTrackingIdentity(
|
|
322
353
|
properties: Record<string, unknown>,
|
|
354
|
+
identity: TrackingIdentity | null = _trackingIdentity,
|
|
323
355
|
): Record<string, unknown> {
|
|
324
|
-
const identity = _trackingIdentity;
|
|
325
356
|
if (!identity) return properties;
|
|
326
357
|
let next = properties;
|
|
327
358
|
const assign = (key: string, value: unknown) => {
|
|
@@ -753,20 +784,27 @@ export function setSentryUser(
|
|
|
753
784
|
user: SentryUser | null,
|
|
754
785
|
orgId?: string | null,
|
|
755
786
|
): void {
|
|
787
|
+
let shouldRetryReplay = false;
|
|
756
788
|
if (user) {
|
|
757
789
|
const userId = user.email || user.id;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
790
|
+
if (userId) {
|
|
791
|
+
_trackingIdentity = {
|
|
792
|
+
userId,
|
|
793
|
+
...(user.email ? { userEmail: user.email } : {}),
|
|
794
|
+
...(user.username ? { userName: user.username } : {}),
|
|
795
|
+
orgId: orgId ?? null,
|
|
796
|
+
};
|
|
797
|
+
} else {
|
|
798
|
+
clearTrackingIdentity();
|
|
799
|
+
}
|
|
800
|
+
shouldRetryReplay = Boolean(user.email);
|
|
766
801
|
} else {
|
|
767
|
-
|
|
802
|
+
clearTrackingIdentity();
|
|
768
803
|
}
|
|
769
804
|
_trackingIdentityResolved = true;
|
|
805
|
+
if (shouldRetryReplay && _sessionReplayOptions?.requireSignedInUser) {
|
|
806
|
+
void startConfiguredSessionReplay(_sessionReplayOptions);
|
|
807
|
+
}
|
|
770
808
|
if (_sentryInitialized) {
|
|
771
809
|
Sentry.setUser(user);
|
|
772
810
|
if (orgId !== undefined) {
|
|
@@ -894,12 +932,16 @@ function sessionReplayEnabledFromEnv(): boolean {
|
|
|
894
932
|
return /^(1|true|yes|on)$/i.test((value ?? "").trim());
|
|
895
933
|
}
|
|
896
934
|
|
|
897
|
-
function sessionReplayRequiresSignedInUserFromEnv(): boolean {
|
|
935
|
+
function sessionReplayRequiresSignedInUserFromEnv(): boolean | undefined {
|
|
898
936
|
const env = (import.meta.env as Record<string, string | undefined>) ?? {};
|
|
899
937
|
const value =
|
|
900
938
|
env.VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH ||
|
|
901
939
|
env.VITE_SESSION_REPLAY_REQUIRE_AUTH;
|
|
902
|
-
|
|
940
|
+
const normalized = (value ?? "").trim();
|
|
941
|
+
if (!normalized) return undefined;
|
|
942
|
+
if (/^(1|true|yes|on)$/i.test(normalized)) return true;
|
|
943
|
+
if (/^(0|false|no|off)$/i.test(normalized)) return false;
|
|
944
|
+
return undefined;
|
|
903
945
|
}
|
|
904
946
|
|
|
905
947
|
function configuredSessionReplayOptions(
|
|
@@ -923,7 +965,8 @@ function configuredSessionReplayOptions(
|
|
|
923
965
|
...options,
|
|
924
966
|
requireSignedInUser:
|
|
925
967
|
options.requireSignedInUser ??
|
|
926
|
-
sessionReplayRequiresSignedInUserFromEnv()
|
|
968
|
+
sessionReplayRequiresSignedInUserFromEnv() ??
|
|
969
|
+
true,
|
|
927
970
|
...(extraProperties ? { extraProperties } : {}),
|
|
928
971
|
};
|
|
929
972
|
};
|
|
@@ -949,7 +992,10 @@ function replayExtraPropertiesWithDefaults(
|
|
|
949
992
|
: {};
|
|
950
993
|
const props = rawProps && typeof rawProps === "object" ? rawProps : {};
|
|
951
994
|
const withDefaults = _getDefaultProps?.("session_replay", props) ?? props;
|
|
952
|
-
return applyTrackingIdentity(
|
|
995
|
+
return applyTrackingIdentity(
|
|
996
|
+
withDefaults,
|
|
997
|
+
_trackingIdentity ?? _sessionReplayIdentitySnapshot,
|
|
998
|
+
);
|
|
953
999
|
};
|
|
954
1000
|
}
|
|
955
1001
|
|
|
@@ -986,6 +1032,7 @@ function maybeInstallSessionReplay(
|
|
|
986
1032
|
if (typeof window === "undefined") return;
|
|
987
1033
|
const options = configuredSessionReplayOptions(config, tracking);
|
|
988
1034
|
if (!options) return;
|
|
1035
|
+
_sessionReplayOptions = options;
|
|
989
1036
|
void startConfiguredSessionReplay(options);
|
|
990
1037
|
}
|
|
991
1038
|
|
|
@@ -993,7 +1040,7 @@ async function waitForSessionReplayAuthIfRequired(
|
|
|
993
1040
|
options: SessionReplayOptions,
|
|
994
1041
|
): Promise<boolean> {
|
|
995
1042
|
if (!options.requireSignedInUser) return true;
|
|
996
|
-
if (_trackingIdentity?.
|
|
1043
|
+
if (_trackingIdentity?.userEmail) return true;
|
|
997
1044
|
try {
|
|
998
1045
|
if (_trackingSessionRefresh) {
|
|
999
1046
|
await _trackingSessionRefresh;
|
|
@@ -1003,21 +1050,25 @@ async function waitForSessionReplayAuthIfRequired(
|
|
|
1003
1050
|
} catch {
|
|
1004
1051
|
// best-effort; missing identity below keeps replay off
|
|
1005
1052
|
}
|
|
1006
|
-
return !!_trackingIdentity?.
|
|
1053
|
+
return !!_trackingIdentity?.userEmail;
|
|
1007
1054
|
}
|
|
1008
1055
|
|
|
1009
1056
|
async function startConfiguredSessionReplay(
|
|
1010
1057
|
options: SessionReplayOptions,
|
|
1011
1058
|
): Promise<SessionReplayStartResult | null> {
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1059
|
+
if (_sessionReplayStartPromise) return _sessionReplayStartPromise;
|
|
1060
|
+
_sessionReplayStartPromise = (async () => {
|
|
1061
|
+
if (!(await waitForSessionReplayAuthIfRequired(options))) {
|
|
1062
|
+
return { started: false, reason: "missing-user-id" as const };
|
|
1063
|
+
}
|
|
1016
1064
|
const mod = await import("./session-replay.js");
|
|
1017
1065
|
return mod.startSessionReplay(options);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1066
|
+
})()
|
|
1067
|
+
.catch(() => ({ started: false, reason: "import-failed" as const }))
|
|
1068
|
+
.finally(() => {
|
|
1069
|
+
_sessionReplayStartPromise = null;
|
|
1070
|
+
});
|
|
1071
|
+
return _sessionReplayStartPromise;
|
|
1021
1072
|
}
|
|
1022
1073
|
|
|
1023
1074
|
export async function startSessionReplay(
|
|
@@ -1044,7 +1095,7 @@ export async function maybeStartSessionReplay(
|
|
|
1044
1095
|
|
|
1045
1096
|
export async function stopSessionReplay(reason = "manual"): Promise<void> {
|
|
1046
1097
|
const mod = await import("./session-replay.js");
|
|
1047
|
-
mod.stopSessionReplay(reason);
|
|
1098
|
+
await mod.stopSessionReplay(reason);
|
|
1048
1099
|
}
|
|
1049
1100
|
|
|
1050
1101
|
function inferTemplateName(properties: Record<string, unknown>): string | null {
|
|
@@ -181,7 +181,7 @@ export function HighlightedCode({
|
|
|
181
181
|
* stay scannable instead of dominating the document, matching the rest of the
|
|
182
182
|
* plan's progressively-disclosed surfaces.
|
|
183
183
|
*/
|
|
184
|
-
export const DEFAULT_CODE_MAX_LINES =
|
|
184
|
+
export const DEFAULT_CODE_MAX_LINES = 30;
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
187
|
* Read-only code surface used across the plan blocks (code tabs, API specs) and
|
|
@@ -35,8 +35,10 @@ export {
|
|
|
35
35
|
} from "./agent-chat.js";
|
|
36
36
|
export {
|
|
37
37
|
saveAgentEngineApiKey,
|
|
38
|
+
saveAgentEngineProviderSettings,
|
|
38
39
|
type AgentEngineProvider,
|
|
39
40
|
type SaveAgentEngineApiKeyOptions,
|
|
41
|
+
type SaveAgentEngineProviderSettingsOptions,
|
|
40
42
|
} from "./agent-engine-key.js";
|
|
41
43
|
export { useAgentChatGenerating } from "./use-agent-chat.js";
|
|
42
44
|
export {
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
} from "./analytics.js";
|
|
6
6
|
|
|
7
7
|
type ReplayEvent = Record<string, unknown>;
|
|
8
|
+
type QueuedReplayEvent = {
|
|
9
|
+
json: string;
|
|
10
|
+
timestampMs: number;
|
|
11
|
+
};
|
|
8
12
|
type ReplayStopFn = () => void;
|
|
9
13
|
export type SessionReplayUrlMatcher =
|
|
10
14
|
| string
|
|
@@ -36,10 +40,12 @@ interface RrwebRecordModule {
|
|
|
36
40
|
|
|
37
41
|
interface SessionReplayState {
|
|
38
42
|
active: boolean;
|
|
43
|
+
startPromise: Promise<SessionReplayStartResult> | null;
|
|
39
44
|
replayId: string | null;
|
|
45
|
+
startedAtMs: number | null;
|
|
40
46
|
sequence: number;
|
|
41
47
|
/** Pre-serialized + scrubbed event JSON strings, ready to splice at flush. */
|
|
42
|
-
queue:
|
|
48
|
+
queue: QueuedReplayEvent[];
|
|
43
49
|
queuedBytes: number;
|
|
44
50
|
flushTimer: number | null;
|
|
45
51
|
maxDurationTimer: number | null;
|
|
@@ -48,11 +54,13 @@ interface SessionReplayState {
|
|
|
48
54
|
restoreUrlMonitor: (() => void) | null;
|
|
49
55
|
removeLifecycleListeners: (() => void) | null;
|
|
50
56
|
options: NormalizedSessionReplayOptions | null;
|
|
57
|
+
lastAuthenticatedProperties: Record<string, unknown> | null;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
interface StoredReplaySession {
|
|
54
61
|
sessionId?: string;
|
|
55
62
|
replayId?: string;
|
|
63
|
+
startedAtMs?: number;
|
|
56
64
|
sequence?: number;
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -201,7 +209,9 @@ function getState(): SessionReplayState {
|
|
|
201
209
|
if (!g[SESSION_REPLAY_STATE_KEY]) {
|
|
202
210
|
g[SESSION_REPLAY_STATE_KEY] = {
|
|
203
211
|
active: false,
|
|
212
|
+
startPromise: null,
|
|
204
213
|
replayId: null,
|
|
214
|
+
startedAtMs: null,
|
|
205
215
|
sequence: 0,
|
|
206
216
|
queue: [],
|
|
207
217
|
queuedBytes: 0,
|
|
@@ -212,6 +222,7 @@ function getState(): SessionReplayState {
|
|
|
212
222
|
restoreUrlMonitor: null,
|
|
213
223
|
removeLifecycleListeners: null,
|
|
214
224
|
options: null,
|
|
225
|
+
lastAuthenticatedProperties: null,
|
|
215
226
|
};
|
|
216
227
|
}
|
|
217
228
|
return g[SESSION_REPLAY_STATE_KEY]!;
|
|
@@ -265,29 +276,43 @@ function writeStoredReplaySession(value: StoredReplaySession): void {
|
|
|
265
276
|
|
|
266
277
|
function getOrCreateReplaySession(sessionId: string): {
|
|
267
278
|
replayId: string;
|
|
279
|
+
startedAtMs: number;
|
|
268
280
|
sequence: number;
|
|
269
281
|
} {
|
|
270
282
|
const parsed = readStoredReplaySession();
|
|
271
283
|
if (parsed?.sessionId === sessionId && parsed.replayId) {
|
|
284
|
+
const startedAtMs =
|
|
285
|
+
typeof parsed.startedAtMs === "number" &&
|
|
286
|
+
Number.isFinite(parsed.startedAtMs) &&
|
|
287
|
+
parsed.startedAtMs > 0
|
|
288
|
+
? parsed.startedAtMs
|
|
289
|
+
: Date.now();
|
|
272
290
|
const sequence =
|
|
273
291
|
typeof parsed.sequence === "number" &&
|
|
274
292
|
Number.isFinite(parsed.sequence) &&
|
|
275
293
|
parsed.sequence >= 0
|
|
276
294
|
? Math.floor(parsed.sequence)
|
|
277
295
|
: 0;
|
|
278
|
-
return { replayId: parsed.replayId, sequence };
|
|
296
|
+
return { replayId: parsed.replayId, startedAtMs, sequence };
|
|
279
297
|
}
|
|
280
298
|
const replayId = generateReplayId();
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
const startedAtMs = Date.now();
|
|
300
|
+
writeStoredReplaySession({ sessionId, replayId, startedAtMs, sequence: 0 });
|
|
301
|
+
return { replayId, startedAtMs, sequence: 0 };
|
|
283
302
|
}
|
|
284
303
|
|
|
285
304
|
function persistReplaySequence(
|
|
286
305
|
sessionId: string,
|
|
287
306
|
replayId: string,
|
|
307
|
+
startedAtMs: number | null,
|
|
288
308
|
sequence: number,
|
|
289
309
|
): void {
|
|
290
|
-
writeStoredReplaySession({
|
|
310
|
+
writeStoredReplaySession({
|
|
311
|
+
sessionId,
|
|
312
|
+
replayId,
|
|
313
|
+
startedAtMs: startedAtMs ?? Date.now(),
|
|
314
|
+
sequence,
|
|
315
|
+
});
|
|
291
316
|
}
|
|
292
317
|
|
|
293
318
|
function clampSamplingRate(value: number | undefined): number {
|
|
@@ -554,6 +579,18 @@ function serializeReplayEvent(event: ReplayEvent): string {
|
|
|
554
579
|
}
|
|
555
580
|
}
|
|
556
581
|
|
|
582
|
+
function replayEventTimestampMs(event: ReplayEvent): number {
|
|
583
|
+
const timestamp = event.timestamp;
|
|
584
|
+
if (typeof timestamp === "number" && Number.isFinite(timestamp)) {
|
|
585
|
+
return timestamp;
|
|
586
|
+
}
|
|
587
|
+
if (typeof timestamp === "string" && timestamp.trim()) {
|
|
588
|
+
const parsed = Date.parse(timestamp);
|
|
589
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
590
|
+
}
|
|
591
|
+
return Date.now();
|
|
592
|
+
}
|
|
593
|
+
|
|
557
594
|
function enqueueReplayEvent(
|
|
558
595
|
state: SessionReplayState,
|
|
559
596
|
event: ReplayEvent,
|
|
@@ -568,7 +605,10 @@ function enqueueReplayEvent(
|
|
|
568
605
|
) {
|
|
569
606
|
void flushSessionReplay("max-bytes");
|
|
570
607
|
}
|
|
571
|
-
state.queue.push(
|
|
608
|
+
state.queue.push({
|
|
609
|
+
json: serialized,
|
|
610
|
+
timestampMs: replayEventTimestampMs(event),
|
|
611
|
+
});
|
|
572
612
|
state.queuedBytes += estimatedBytes;
|
|
573
613
|
if (state.queue.length >= state.options.maxEventsPerBatch) {
|
|
574
614
|
void flushSessionReplay("max-events");
|
|
@@ -593,27 +633,73 @@ function replayString(value: unknown): string | undefined {
|
|
|
593
633
|
return typeof value === "string" && value.trim() ? value : undefined;
|
|
594
634
|
}
|
|
595
635
|
|
|
636
|
+
function replayEmail(value: unknown): string | undefined {
|
|
637
|
+
const raw = replayString(value);
|
|
638
|
+
return raw && raw.includes("@") ? raw : undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function replayUserEmail(
|
|
642
|
+
properties: Record<string, unknown> | undefined,
|
|
643
|
+
): string | undefined {
|
|
644
|
+
return (
|
|
645
|
+
replayEmail(properties?.userEmail ?? properties?.user_email) ||
|
|
646
|
+
replayEmail(properties?.email) ||
|
|
647
|
+
replayEmail(properties?.userId ?? properties?.user_id)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function replayPropertiesForUpload(
|
|
652
|
+
state: SessionReplayState,
|
|
653
|
+
options: NormalizedSessionReplayOptions,
|
|
654
|
+
): Record<string, unknown> | undefined {
|
|
655
|
+
const properties = replayExtraProperties(options);
|
|
656
|
+
if (replayUserEmail(properties)) {
|
|
657
|
+
state.lastAuthenticatedProperties = properties ? { ...properties } : null;
|
|
658
|
+
return properties;
|
|
659
|
+
}
|
|
660
|
+
if (options.requireSignedInUser && state.lastAuthenticatedProperties) {
|
|
661
|
+
return state.lastAuthenticatedProperties;
|
|
662
|
+
}
|
|
663
|
+
return properties;
|
|
664
|
+
}
|
|
665
|
+
|
|
596
666
|
function buildReplayBody(
|
|
597
667
|
state: SessionReplayState,
|
|
598
668
|
reason: string,
|
|
599
|
-
|
|
669
|
+
events: QueuedReplayEvent[],
|
|
600
670
|
): string | null {
|
|
601
671
|
const options = state.options;
|
|
602
672
|
if (!options || !state.replayId) return null;
|
|
603
673
|
const sessionId = getAnalyticsSessionId();
|
|
604
674
|
if (!sessionId) return null;
|
|
605
|
-
const properties =
|
|
606
|
-
const
|
|
675
|
+
const properties = replayPropertiesForUpload(state, options);
|
|
676
|
+
const userEmail = replayUserEmail(properties);
|
|
677
|
+
if (options.requireSignedInUser && !userEmail) return null;
|
|
678
|
+
const userId =
|
|
679
|
+
userEmail || replayString(properties?.userId ?? properties?.user_id);
|
|
680
|
+
const eventTimestamps = events.map((event) => event.timestampMs);
|
|
681
|
+
const nowMs = Date.now();
|
|
682
|
+
const startedAtMs =
|
|
683
|
+
state.startedAtMs ??
|
|
684
|
+
(eventTimestamps.length ? Math.min(...eventTimestamps) : nowMs);
|
|
685
|
+
const endedAtMs = eventTimestamps.length
|
|
686
|
+
? Math.max(...eventTimestamps)
|
|
687
|
+
: nowMs;
|
|
607
688
|
const envelope = {
|
|
608
689
|
publicKey: options.publicKey,
|
|
609
690
|
type: "session_replay",
|
|
610
691
|
replayId: state.replayId,
|
|
611
692
|
sessionId,
|
|
612
693
|
...(userId ? { userId } : {}),
|
|
694
|
+
...(userEmail ? { userEmail } : {}),
|
|
613
695
|
anonymousId: getAnalyticsAnonymousId(),
|
|
614
696
|
sequence: state.sequence,
|
|
615
697
|
reason,
|
|
616
698
|
status: isFinalFlushReason(reason) ? "completed" : "active",
|
|
699
|
+
eventCount: events.length,
|
|
700
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
701
|
+
endedAt: new Date(endedAtMs).toISOString(),
|
|
702
|
+
durationMs: Math.max(0, endedAtMs - startedAtMs),
|
|
617
703
|
privacyMode: "mask-inputs-and-selected-text",
|
|
618
704
|
url:
|
|
619
705
|
typeof window !== "undefined"
|
|
@@ -623,11 +709,18 @@ function buildReplayBody(
|
|
|
623
709
|
properties,
|
|
624
710
|
};
|
|
625
711
|
state.sequence += 1;
|
|
626
|
-
persistReplaySequence(
|
|
712
|
+
persistReplaySequence(
|
|
713
|
+
sessionId,
|
|
714
|
+
state.replayId,
|
|
715
|
+
state.startedAtMs,
|
|
716
|
+
state.sequence,
|
|
717
|
+
);
|
|
627
718
|
// Events are already serialized+scrubbed JSON strings; splice them into the
|
|
628
719
|
// envelope without re-serializing the (potentially large) events array.
|
|
629
720
|
const envelopeJson = JSON.stringify(envelope);
|
|
630
|
-
return `${envelopeJson.slice(0, -1)},"events":[${
|
|
721
|
+
return `${envelopeJson.slice(0, -1)},"events":[${events
|
|
722
|
+
.map((event) => event.json)
|
|
723
|
+
.join(",")}]}`;
|
|
631
724
|
}
|
|
632
725
|
|
|
633
726
|
interface ReplayUploadBody {
|
|
@@ -698,6 +791,7 @@ async function sendReplayUpload(
|
|
|
698
791
|
|
|
699
792
|
function isFinalFlushReason(reason: string): boolean {
|
|
700
793
|
return [
|
|
794
|
+
"auth-cleared",
|
|
701
795
|
"manual",
|
|
702
796
|
"pagehide",
|
|
703
797
|
"beforeunload",
|
|
@@ -712,7 +806,14 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
|
|
|
712
806
|
const events = state.queue.splice(0, state.queue.length);
|
|
713
807
|
state.queuedBytes = 0;
|
|
714
808
|
const body = buildReplayBody(state, reason, events);
|
|
715
|
-
if (!body || !state.options)
|
|
809
|
+
if (!body || !state.options) {
|
|
810
|
+
state.queue = events.concat(state.queue);
|
|
811
|
+
state.queuedBytes += events.reduce(
|
|
812
|
+
(total, event) => total + event.json.length,
|
|
813
|
+
0,
|
|
814
|
+
);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
716
817
|
state.flushing = true;
|
|
717
818
|
try {
|
|
718
819
|
await sendReplayUpload(state.options, body);
|
|
@@ -789,10 +890,9 @@ export async function startSessionReplay(
|
|
|
789
890
|
if (!sampled) {
|
|
790
891
|
return { started: false, reason: "sampled-out", sessionId, sampled };
|
|
791
892
|
}
|
|
893
|
+
const initialProperties = replayExtraProperties(normalized);
|
|
792
894
|
if (normalized.requireSignedInUser) {
|
|
793
|
-
|
|
794
|
-
const userId = replayString(properties?.userId ?? properties?.user_id);
|
|
795
|
-
if (!userId) {
|
|
895
|
+
if (!replayUserEmail(initialProperties)) {
|
|
796
896
|
return {
|
|
797
897
|
started: false,
|
|
798
898
|
reason: "missing-user-id",
|
|
@@ -815,6 +915,40 @@ export async function startSessionReplay(
|
|
|
815
915
|
sampled,
|
|
816
916
|
};
|
|
817
917
|
}
|
|
918
|
+
if (state.startPromise) return state.startPromise;
|
|
919
|
+
|
|
920
|
+
let startPromise: Promise<SessionReplayStartResult>;
|
|
921
|
+
startPromise = startSessionReplayRecorder(
|
|
922
|
+
state,
|
|
923
|
+
normalized,
|
|
924
|
+
sessionId,
|
|
925
|
+
sampled,
|
|
926
|
+
initialProperties,
|
|
927
|
+
).finally(() => {
|
|
928
|
+
if (state.startPromise === startPromise) {
|
|
929
|
+
state.startPromise = null;
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
state.startPromise = startPromise;
|
|
933
|
+
return startPromise;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function startSessionReplayRecorder(
|
|
937
|
+
state: SessionReplayState,
|
|
938
|
+
normalized: NormalizedSessionReplayOptions,
|
|
939
|
+
sessionId: string,
|
|
940
|
+
sampled: boolean,
|
|
941
|
+
initialProperties: Record<string, unknown> | undefined,
|
|
942
|
+
): Promise<SessionReplayStartResult> {
|
|
943
|
+
if (state.active && state.replayId) {
|
|
944
|
+
return {
|
|
945
|
+
started: true,
|
|
946
|
+
reason: "already-active",
|
|
947
|
+
replayId: state.replayId,
|
|
948
|
+
sessionId,
|
|
949
|
+
sampled,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
818
952
|
|
|
819
953
|
let rrweb: RrwebRecordModule;
|
|
820
954
|
try {
|
|
@@ -826,10 +960,14 @@ export async function startSessionReplay(
|
|
|
826
960
|
const replaySession = getOrCreateReplaySession(sessionId);
|
|
827
961
|
state.options = normalized;
|
|
828
962
|
state.replayId = replaySession.replayId;
|
|
963
|
+
state.startedAtMs = replaySession.startedAtMs;
|
|
829
964
|
state.sequence = replaySession.sequence;
|
|
830
965
|
state.queue = [];
|
|
831
966
|
state.queuedBytes = 0;
|
|
832
967
|
state.stopRecorder = null;
|
|
968
|
+
state.lastAuthenticatedProperties = replayUserEmail(initialProperties)
|
|
969
|
+
? { ...initialProperties }
|
|
970
|
+
: null;
|
|
833
971
|
state.active = true;
|
|
834
972
|
|
|
835
973
|
try {
|
|
@@ -868,6 +1006,8 @@ export async function startSessionReplay(
|
|
|
868
1006
|
state.active = false;
|
|
869
1007
|
state.options = null;
|
|
870
1008
|
state.replayId = null;
|
|
1009
|
+
state.startedAtMs = null;
|
|
1010
|
+
state.lastAuthenticatedProperties = null;
|
|
871
1011
|
return { started: false, reason: "record-failed", sessionId, sampled };
|
|
872
1012
|
}
|
|
873
1013
|
state.stopRecorder = stopRecorder;
|
|
@@ -891,11 +1031,13 @@ export async function startSessionReplay(
|
|
|
891
1031
|
state.active = false;
|
|
892
1032
|
state.options = null;
|
|
893
1033
|
state.replayId = null;
|
|
1034
|
+
state.startedAtMs = null;
|
|
1035
|
+
state.lastAuthenticatedProperties = null;
|
|
894
1036
|
return { started: false, reason: "record-failed", sessionId, sampled };
|
|
895
1037
|
}
|
|
896
1038
|
}
|
|
897
1039
|
|
|
898
|
-
export function stopSessionReplay(reason = "manual"): void {
|
|
1040
|
+
export async function stopSessionReplay(reason = "manual"): Promise<void> {
|
|
899
1041
|
const state = getState();
|
|
900
1042
|
if (!state.active) return;
|
|
901
1043
|
state.active = false;
|
|
@@ -915,7 +1057,7 @@ export function stopSessionReplay(reason = "manual"): void {
|
|
|
915
1057
|
}
|
|
916
1058
|
state.restoreUrlMonitor?.();
|
|
917
1059
|
state.removeLifecycleListeners?.();
|
|
918
|
-
|
|
1060
|
+
await flushSessionReplay(reason);
|
|
919
1061
|
}
|
|
920
1062
|
|
|
921
1063
|
export function maybeStartSessionReplay(
|