@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.
Files changed (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/deployment.md +133 -0
  4. package/corpus/core/docs/content/faq.md +1 -1
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -0
  6. package/corpus/core/docs/content/tracking.md +3 -3
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
  9. package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
  10. package/corpus/core/src/agent/engine/registry.ts +59 -11
  11. package/corpus/core/src/client/FeedbackButton.tsx +11 -1
  12. package/corpus/core/src/client/agent-engine-key.ts +57 -10
  13. package/corpus/core/src/client/analytics.ts +78 -27
  14. package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
  15. package/corpus/core/src/client/index.ts +2 -0
  16. package/corpus/core/src/client/session-replay.ts +159 -17
  17. package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
  18. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
  19. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
  20. package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
  21. package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
  22. package/corpus/core/src/server/core-routes-plugin.ts +26 -2
  23. package/corpus/core/src/styles/blocks.css +23 -7
  24. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
  25. package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +5 -10
  26. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
  29. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
  31. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
  37. package/corpus/templates/analytics/netlify.toml +4 -0
  38. package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
  39. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
  40. package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
  41. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
  42. package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
  43. package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
  44. package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  45. package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
  46. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +20 -4
  47. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -2
  48. package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-27-desktop-storage-setup-flow.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +6 -0
  52. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  53. package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
  54. package/corpus/templates/clips/desktop/package.json +1 -0
  55. package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
  56. package/corpus/templates/clips/desktop/src/app.tsx +32 -3
  57. package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
  58. package/corpus/templates/clips/desktop/src/main.tsx +2 -0
  59. package/corpus/templates/clips/desktop/src/styles.css +67 -0
  60. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
  61. package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
  62. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
  64. package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
  65. package/corpus/templates/clips/desktop/vite.config.ts +66 -0
  66. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -1
  67. package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  68. package/corpus/templates/forms/actions/export-responses.ts +3 -2
  69. package/corpus/templates/forms/actions/list-responses.ts +2 -1
  70. package/corpus/templates/forms/actions/response-insights.ts +13 -4
  71. package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
  72. package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
  73. package/corpus/templates/forms/server/lib/integrations.ts +9 -6
  74. package/corpus/templates/forms/shared/submitter-email.ts +20 -0
  75. package/corpus/templates/forms/shared/types.ts +1 -1
  76. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  78. package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  79. package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
  80. package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
  81. package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
  82. package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
  83. package/dist/agent/engine/provider-env-vars.d.ts +1 -0
  84. package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
  85. package/dist/agent/engine/provider-env-vars.js +1 -0
  86. package/dist/agent/engine/provider-env-vars.js.map +1 -1
  87. package/dist/agent/engine/registry.d.ts.map +1 -1
  88. package/dist/agent/engine/registry.js +39 -12
  89. package/dist/agent/engine/registry.js.map +1 -1
  90. package/dist/client/FeedbackButton.d.ts.map +1 -1
  91. package/dist/client/FeedbackButton.js +6 -1
  92. package/dist/client/FeedbackButton.js.map +1 -1
  93. package/dist/client/agent-engine-key.d.ts +14 -0
  94. package/dist/client/agent-engine-key.d.ts.map +1 -1
  95. package/dist/client/agent-engine-key.js +35 -10
  96. package/dist/client/agent-engine-key.js.map +1 -1
  97. package/dist/client/analytics.d.ts.map +1 -1
  98. package/dist/client/analytics.js +71 -24
  99. package/dist/client/analytics.js.map +1 -1
  100. package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
  101. package/dist/client/blocks/library/HighlightedCode.js +1 -1
  102. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  103. package/dist/client/index.d.ts +1 -1
  104. package/dist/client/index.d.ts.map +1 -1
  105. package/dist/client/index.js +1 -1
  106. package/dist/client/index.js.map +1 -1
  107. package/dist/client/session-replay.d.ts +1 -1
  108. package/dist/client/session-replay.d.ts.map +1 -1
  109. package/dist/client/session-replay.js +115 -17
  110. package/dist/client/session-replay.js.map +1 -1
  111. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  112. package/dist/client/settings/SettingsPanel.js +42 -6
  113. package/dist/client/settings/SettingsPanel.js.map +1 -1
  114. package/dist/collab/routes.d.ts +2 -2
  115. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  116. package/dist/notifications/routes.d.ts +1 -1
  117. package/dist/observability/routes.d.ts +7 -7
  118. package/dist/progress/routes.d.ts +1 -1
  119. package/dist/resources/handlers.d.ts +1 -1
  120. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  121. package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
  122. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  123. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  124. package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
  125. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  126. package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
  127. package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
  128. package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
  129. package/dist/server/agent-engine-api-key-route.d.ts +5 -5
  130. package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
  131. package/dist/server/agent-engine-api-key-route.js +71 -11
  132. package/dist/server/agent-engine-api-key-route.js.map +1 -1
  133. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  134. package/dist/server/core-routes-plugin.js +20 -3
  135. package/dist/server/core-routes-plugin.js.map +1 -1
  136. package/dist/styles/blocks.css +23 -7
  137. package/docs/content/deployment.md +133 -0
  138. package/docs/content/faq.md +1 -1
  139. package/docs/content/multi-app-workspace.md +2 -0
  140. package/docs/content/tracking.md +3 -3
  141. 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
- _trackingIdentity = null;
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
- _trackingIdentity = null;
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
- _trackingIdentity = null;
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
- _trackingIdentity = userId
759
- ? {
760
- userId,
761
- ...(user.email ? { userEmail: user.email } : {}),
762
- ...(user.username ? { userName: user.username } : {}),
763
- orgId: orgId ?? null,
764
- }
765
- : null;
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
- _trackingIdentity = null;
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
- return /^(1|true|yes|on)$/i.test((value ?? "").trim());
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(withDefaults);
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?.userId) return true;
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?.userId;
1053
+ return !!_trackingIdentity?.userEmail;
1007
1054
  }
1008
1055
 
1009
1056
  async function startConfiguredSessionReplay(
1010
1057
  options: SessionReplayOptions,
1011
1058
  ): Promise<SessionReplayStartResult | null> {
1012
- if (!(await waitForSessionReplayAuthIfRequired(options))) {
1013
- return { started: false, reason: "missing-user-id" };
1014
- }
1015
- try {
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
- } catch {
1019
- return { started: false, reason: "import-failed" };
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 = 40;
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: string[];
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
- writeStoredReplaySession({ sessionId, replayId, sequence: 0 });
282
- return { replayId, sequence: 0 };
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({ sessionId, replayId, sequence });
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(serialized);
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
- eventJsonParts: string[],
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 = replayExtraProperties(options);
606
- const userId = replayString(properties?.userId ?? properties?.user_id);
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(sessionId, state.replayId, state.sequence);
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":[${eventJsonParts.join(",")}]}`;
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) return;
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
- const properties = replayExtraProperties(normalized);
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
- void flushSessionReplay(reason);
1060
+ await flushSessionReplay(reason);
919
1061
  }
920
1062
 
921
1063
  export function maybeStartSessionReplay(