@agent-native/core 0.163.0 → 0.163.2

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 (43) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/plugins/collab.ts +1 -0
  3. package/corpus/templates/slides/DEVELOPING.md +17 -0
  4. package/corpus/templates/slides/actions/get-deck-access-status.ts +67 -0
  5. package/corpus/templates/slides/actions/request-deck-access.ts +569 -0
  6. package/corpus/templates/slides/agent-native.json +3 -0
  7. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -0
  8. package/corpus/templates/slides/app/hooks/use-deck-access.ts +51 -0
  9. package/corpus/templates/slides/app/i18n/en-US.ts +12 -0
  10. package/corpus/templates/slides/app/lib/deck-editor-loading.ts +7 -0
  11. package/corpus/templates/slides/app/pages/DeckEditor.tsx +172 -32
  12. package/corpus/templates/slides/changelog/2026-08-18-private-deck-links-now-explain-access-and-let-viewers-notify.md +6 -0
  13. package/corpus/templates/slides/server/db/schema.ts +10 -0
  14. package/corpus/templates/slides/server/plugins/db.ts +17 -0
  15. package/dist/client/analytics.d.ts +1 -0
  16. package/dist/client/analytics.js +19 -5
  17. package/dist/client/session-replay.js +349 -100
  18. package/dist/collab/routes.d.ts +1 -1
  19. package/dist/collab/storage.d.ts +2 -0
  20. package/dist/collab/storage.js +7 -0
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/config.js +11 -1
  23. package/dist/deploy/build.d.ts +1 -1
  24. package/dist/deploy/build.js +89 -12
  25. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  26. package/dist/observability/routes.d.ts +1 -1
  27. package/dist/observability/tracking-identity.js +2 -0
  28. package/dist/progress/routes.d.ts +1 -1
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/secrets/routes.d.ts +9 -9
  31. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  32. package/dist/server/collab-plugin.d.ts +6 -0
  33. package/dist/server/collab-plugin.js +27 -6
  34. package/dist/server/deploy-environment.d.ts +1 -1
  35. package/dist/server/deploy-environment.js +40 -2
  36. package/dist/server/realtime-token.d.ts +1 -1
  37. package/dist/server/sentry-config.js +8 -4
  38. package/dist/server/sentry.js +4 -0
  39. package/dist/server/transcribe-voice.d.ts +1 -1
  40. package/dist/tracking/registry.js +8 -6
  41. package/dist/vite/client.js +5 -0
  42. package/docs/content/deployment.mdx +19 -5
  43. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ } from "@agent-native/core/client/hooks";
5
+ import { toast } from "sonner";
6
+
7
+ export type DeckAccessStatusResponse = {
8
+ exists: boolean;
9
+ hasAccess: boolean;
10
+ signedIn: boolean;
11
+ viewerEmail: string | null;
12
+ viewerName: string | null;
13
+ role: "owner" | "viewer" | "commenter" | "editor" | "admin" | null;
14
+ visibility: "private" | "org" | "public" | null;
15
+ };
16
+
17
+ export function useDeckAccessStatus(deckId?: string) {
18
+ return useActionQuery<DeckAccessStatusResponse>(
19
+ "get-deck-access-status",
20
+ { deckId: deckId ?? "" },
21
+ {
22
+ enabled: Boolean(deckId),
23
+ retry: false,
24
+ },
25
+ );
26
+ }
27
+
28
+ export type RequestDeckAccessResult = {
29
+ ok: true;
30
+ alreadyHasAccess: boolean;
31
+ notifiedOwner: boolean;
32
+ requestId?: string;
33
+ message: string;
34
+ };
35
+
36
+ function showActionError(message: string) {
37
+ return (error: Error) => {
38
+ toast.error(
39
+ error.message
40
+ ? error.message.replace(/^Action [\w-]+ failed:\s*/, "")
41
+ : message,
42
+ );
43
+ };
44
+ }
45
+
46
+ export function useRequestDeckAccess() {
47
+ return useActionMutation<RequestDeckAccessResult, { deckId: string }>(
48
+ "request-deck-access",
49
+ { onError: showActionError("Failed to request access") },
50
+ );
51
+ }
@@ -565,6 +565,18 @@ const messages = {
565
565
  "This link points to a team presentation. Join the team shown above and the deck will open here automatically.",
566
566
  deckUnavailableDescription:
567
567
  "This deck may have been removed, or your account does not have access to it.",
568
+ privateDeckTitle: "This deck is private",
569
+ privateDeckDescription:
570
+ "Ask the owner for access to open this deck. They’ll be notified when you send a request.",
571
+ signedInAs: "You're signed in as",
572
+ accessRequestSentDescription:
573
+ "The owner has been notified. You can open this deck once access is granted.",
574
+ accessRequestRecordedDescription:
575
+ "Your request has been recorded. You can open this deck once access is granted.",
576
+ requestAccessPending: "Sending request...",
577
+ accessRequestSent: "Request sent",
578
+ requestAccess: "Request access",
579
+ signInToRequestAccess: "Sign in to request access",
568
580
  backToDecks: "Back to Decks",
569
581
  tryAgain: "Try again",
570
582
  imageUploadFailed: "Image upload failed",
@@ -12,6 +12,7 @@ export function shouldShowDeckEditorSkeleton({
12
12
  accessCheckKey,
13
13
  checkedAccessKey,
14
14
  retrying,
15
+ privateDeckAccessConfirmed,
15
16
  }: {
16
17
  deckFound: boolean;
17
18
  decksLoading: boolean;
@@ -19,7 +20,13 @@ export function shouldShowDeckEditorSkeleton({
19
20
  accessCheckKey: string | null;
20
21
  checkedAccessKey: string | null;
21
22
  retrying: boolean;
23
+ privateDeckAccessConfirmed: boolean;
22
24
  }): boolean {
25
+ // A private deck intentionally returns 404 from the content action. The
26
+ // metadata-only access check is the source of truth for this state, so do
27
+ // not leave the user on the editor skeleton while that protected fetch
28
+ // settles.
29
+ if (privateDeckAccessConfirmed) return false;
23
30
  if (decksLoading) return true;
24
31
  if (deckFound || !accessCheckKey) return false;
25
32
  return orgLoading || checkedAccessKey !== accessCheckKey || retrying;
@@ -1,5 +1,8 @@
1
1
  import { useGuidedQuestionFlow } from "@agent-native/core/client/agent-chat";
2
- import { appBasePath } from "@agent-native/core/client/api-path";
2
+ import {
3
+ agentNativePath,
4
+ appBasePath,
5
+ } from "@agent-native/core/client/api-path";
3
6
  import {
4
7
  useCollaborativeDoc,
5
8
  emailToColor,
@@ -8,6 +11,7 @@ import {
8
11
  import { useSession } from "@agent-native/core/client/hooks";
9
12
  import { useT } from "@agent-native/core/client/i18n";
10
13
  import { useOrg } from "@agent-native/core/client/org";
14
+ import { buildSignInReturnHref } from "@agent-native/core/client/ui";
11
15
  import {
12
16
  DndContext,
13
17
  closestCenter,
@@ -17,9 +21,13 @@ import {
17
21
  DragEndEvent,
18
22
  } from "@dnd-kit/core";
19
23
  import {
24
+ IconAt,
20
25
  IconArrowLeft,
21
26
  IconLock,
27
+ IconLoader2,
28
+ IconLogin2,
22
29
  IconRefresh,
30
+ IconUserPlus,
23
31
  IconUsersGroup,
24
32
  } from "@tabler/icons-react";
25
33
  import { nanoid } from "nanoid";
@@ -67,6 +75,11 @@ import {
67
75
  useSaveState,
68
76
  } from "@/context/DeckContext";
69
77
  import { useAgentGenerating } from "@/hooks/use-agent-generating";
78
+ import {
79
+ useDeckAccessStatus,
80
+ useRequestDeckAccess,
81
+ type DeckAccessStatusResponse,
82
+ } from "@/hooks/use-deck-access";
70
83
  import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
71
84
  import { useDeckPresence } from "@/hooks/use-deck-presence";
72
85
  import { useDeckRole } from "@/hooks/use-deck-role";
@@ -109,37 +122,70 @@ import { shortcutLabel } from "@/lib/utils";
109
122
  type EditorSidePanel = "comments" | null;
110
123
 
111
124
  function MissingDeckAccessPane({
125
+ accessStatus,
126
+ accessStatusError,
127
+ accessStatusLoading,
112
128
  hasTeamJoinOption,
113
129
  orgLoading,
114
130
  orgError,
131
+ requestAccessPending,
132
+ accessRequestSent,
133
+ accessRequestNotified,
134
+ signedIn,
135
+ viewerEmail,
115
136
  refreshing,
137
+ onRequestAccess,
138
+ onSignIn,
116
139
  onRetry,
117
140
  onBack,
118
141
  }: {
142
+ accessStatus: DeckAccessStatusResponse | null;
143
+ accessStatusError: boolean;
144
+ accessStatusLoading: boolean;
119
145
  hasTeamJoinOption: boolean;
120
146
  orgLoading: boolean;
121
147
  orgError: boolean;
148
+ requestAccessPending: boolean;
149
+ accessRequestSent: boolean;
150
+ accessRequestNotified: boolean;
151
+ signedIn: boolean;
152
+ viewerEmail: string | null;
122
153
  refreshing: boolean;
154
+ onRequestAccess: () => void;
155
+ onSignIn: () => void;
123
156
  onRetry: () => void;
124
157
  onBack: () => void;
125
158
  }) {
126
159
  const t = useT();
160
+ const privateDeck = Boolean(
161
+ accessStatus?.exists &&
162
+ !accessStatus.hasAccess &&
163
+ accessStatus.visibility === "private",
164
+ );
165
+ const checkingAccess = !privateDeck && (accessStatusLoading || orgLoading);
166
+ const accessCheckFailed = accessStatusError || orgError;
127
167
  const Icon =
128
- hasTeamJoinOption || orgLoading || orgError ? IconUsersGroup : IconLock;
129
- const title = orgLoading
168
+ privateDeck || (!hasTeamJoinOption && !checkingAccess && !accessCheckFailed)
169
+ ? IconLock
170
+ : IconUsersGroup;
171
+ const title = checkingAccess
130
172
  ? t("deckEditor.lookingForDeck")
131
- : orgError
132
- ? t("deckEditor.teamAccessCheckFailed")
133
- : hasTeamJoinOption
134
- ? t("deckEditor.joinTeamToOpen")
135
- : t("deckEditor.deckUnavailable");
136
- const description = orgLoading
173
+ : privateDeck
174
+ ? t("deckEditor.privateDeckTitle")
175
+ : accessCheckFailed
176
+ ? t("deckEditor.teamAccessCheckFailed")
177
+ : hasTeamJoinOption
178
+ ? t("deckEditor.joinTeamToOpen")
179
+ : t("deckEditor.deckUnavailable");
180
+ const description = checkingAccess
137
181
  ? t("deckEditor.checkingSharedAccess")
138
- : orgError
139
- ? t("deckEditor.verifySharedAccessFailed")
140
- : hasTeamJoinOption
141
- ? t("deckEditor.joinTeamDescription")
142
- : t("deckEditor.deckUnavailableDescription");
182
+ : privateDeck
183
+ ? t("deckEditor.privateDeckDescription")
184
+ : accessCheckFailed
185
+ ? t("deckEditor.verifySharedAccessFailed")
186
+ : hasTeamJoinOption
187
+ ? t("deckEditor.joinTeamDescription")
188
+ : t("deckEditor.deckUnavailableDescription");
143
189
 
144
190
  return (
145
191
  <div className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
@@ -153,21 +199,64 @@ function MissingDeckAccessPane({
153
199
  </div>
154
200
  </div>
155
201
  <p className="text-sm leading-6 text-muted-foreground">{description}</p>
156
- <div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
157
- <Button type="button" variant="outline" onClick={onBack}>
158
- <IconArrowLeft className="size-4" />
159
- {t("deckEditor.backToDecks")}
160
- </Button>
161
- <Button
162
- type="button"
163
- onClick={onRetry}
164
- disabled={refreshing || orgLoading}
165
- >
166
- <IconRefresh
167
- className={refreshing ? "size-4 animate-spin" : "size-4"}
168
- />
169
- {t("deckEditor.tryAgain")}
170
- </Button>
202
+ {privateDeck && viewerEmail ? (
203
+ <div className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/35 px-3 py-2 text-sm">
204
+ <IconAt className="size-4 shrink-0 text-muted-foreground" />
205
+ <span className="min-w-0 truncate text-muted-foreground">
206
+ {t("deckEditor.signedInAs")}{" "}
207
+ <span className="font-medium text-foreground">{viewerEmail}</span>
208
+ </span>
209
+ </div>
210
+ ) : null}
211
+ {privateDeck && accessRequestSent ? (
212
+ <div className="mt-3 rounded-md border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300">
213
+ {t(
214
+ accessRequestNotified
215
+ ? "deckEditor.accessRequestSentDescription"
216
+ : "deckEditor.accessRequestRecordedDescription",
217
+ )}
218
+ </div>
219
+ ) : null}
220
+ <div className="mt-5 flex flex-col gap-2">
221
+ {privateDeck ? (
222
+ <Button
223
+ type="button"
224
+ onClick={signedIn ? onRequestAccess : onSignIn}
225
+ disabled={requestAccessPending || accessRequestSent}
226
+ >
227
+ {requestAccessPending ? (
228
+ <IconLoader2 className="size-4 animate-spin" />
229
+ ) : signedIn ? (
230
+ <IconUserPlus className="size-4" />
231
+ ) : (
232
+ <IconLogin2 className="size-4" />
233
+ )}
234
+ {requestAccessPending
235
+ ? t("deckEditor.requestAccessPending")
236
+ : accessRequestSent
237
+ ? t("deckEditor.accessRequestSent")
238
+ : signedIn
239
+ ? t("deckEditor.requestAccess")
240
+ : t("deckEditor.signInToRequestAccess")}
241
+ </Button>
242
+ ) : null}
243
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
244
+ <Button type="button" variant="outline" onClick={onBack}>
245
+ <IconArrowLeft className="size-4" />
246
+ {t("deckEditor.backToDecks")}
247
+ </Button>
248
+ <Button
249
+ type="button"
250
+ variant={privateDeck ? "ghost" : "default"}
251
+ onClick={onRetry}
252
+ disabled={refreshing || checkingAccess}
253
+ >
254
+ <IconRefresh
255
+ className={refreshing ? "size-4 animate-spin" : "size-4"}
256
+ />
257
+ {t("deckEditor.tryAgain")}
258
+ </Button>
259
+ </div>
171
260
  </div>
172
261
  </div>
173
262
  </div>
@@ -200,6 +289,7 @@ export default function DeckEditor() {
200
289
  const { id } = useParams<{ id: string }>();
201
290
  const navigate = useNavigate();
202
291
  const [searchParams, setSearchParams] = useSearchParams();
292
+ const { session, isLoading: sessionLoading } = useSession();
203
293
  const {
204
294
  getDeck,
205
295
  reloadDecks,
@@ -219,6 +309,8 @@ export default function DeckEditor() {
219
309
  loading,
220
310
  loadError,
221
311
  } = useDecks();
312
+ const deckAccessStatusQuery = useDeckAccessStatus(id);
313
+ const requestDeckAccessMutation = useRequestDeckAccess();
222
314
  const [activeSlideId, setActiveSlideId] = useState<string | null>(null);
223
315
  const [inlineEditActive, setInlineEditActive] = useState(false);
224
316
  const [addSlideGenerating, setAddSlideGenerating] = useState(false);
@@ -294,6 +386,10 @@ export default function DeckEditor() {
294
386
  const [wideContextToolbarSlot, setWideContextToolbarSlot] =
295
387
  useState<HTMLDivElement | null>(null);
296
388
  const [retryingMissingDeck, setRetryingMissingDeck] = useState(false);
389
+ const [accessRequestSentDeckId, setAccessRequestSentDeckId] = useState<
390
+ string | null
391
+ >(null);
392
+ const [accessRequestNotified, setAccessRequestNotified] = useState(false);
297
393
  const [checkedDeckAccessKey, setCheckedDeckAccessKey] = useState<
298
394
  string | null
299
395
  >(null);
@@ -349,6 +445,7 @@ export default function DeckEditor() {
349
445
  const uploadInputRef = useRef<HTMLInputElement>(null);
350
446
 
351
447
  const deck = getDeck(id || "");
448
+ const deckAccessStatus = deckAccessStatusQuery.data ?? null;
352
449
  const fitDims = getAspectRatioDims(deck?.aspectRatio);
353
450
  const currentDeckAccessKey = deckAccessCheckKey(id, org?.orgId);
354
451
  const hasTeamJoinOption =
@@ -511,6 +608,32 @@ export default function DeckEditor() {
511
608
  }
512
609
  }, [refetchOrg, reloadDecks]);
513
610
 
611
+ const openSignIn = useCallback(() => {
612
+ window.location.href = buildSignInReturnHref();
613
+ }, []);
614
+
615
+ const requestDeckAccess = useCallback(() => {
616
+ if (!id) return;
617
+ requestDeckAccessMutation.mutate(
618
+ { deckId: id },
619
+ {
620
+ onSuccess: (result) => {
621
+ setAccessRequestSentDeckId(id);
622
+ setAccessRequestNotified(result.notifiedOwner);
623
+ toast.success(result.message);
624
+ if (result.alreadyHasAccess) void reloadDecks();
625
+ },
626
+ },
627
+ );
628
+ }, [id, reloadDecks, requestDeckAccessMutation]);
629
+
630
+ useEffect(() => {
631
+ if (accessRequestSentDeckId && accessRequestSentDeckId !== id) {
632
+ setAccessRequestSentDeckId(null);
633
+ setAccessRequestNotified(false);
634
+ }
635
+ }, [accessRequestSentDeckId, id]);
636
+
514
637
  // The final generation write can race the last sync event. Pull the
515
638
  // authoritative open deck when the run settles so a stale canvas does not
516
639
  // require a browser refresh to reveal completed slides.
@@ -1204,7 +1327,6 @@ export default function DeckEditor() {
1204
1327
  useRef<typeof deck extends undefined ? null : any>(null);
1205
1328
 
1206
1329
  // Session for collab user identity
1207
- const { session } = useSession();
1208
1330
  const currentUser = session?.email
1209
1331
  ? {
1210
1332
  email: session.email,
@@ -1242,7 +1364,7 @@ export default function DeckEditor() {
1242
1364
  recentEdits: deckRecentEdits,
1243
1365
  awareness: deckPresenceAwareness,
1244
1366
  } = useDeckPresence({
1245
- deckId: id ?? null,
1367
+ deckId: deck ? (id ?? null) : null,
1246
1368
  activeSlideId: activeSlideId,
1247
1369
  user: currentUser,
1248
1370
  });
@@ -1254,7 +1376,10 @@ export default function DeckEditor() {
1254
1376
  const agentActive = generating || deckAgentActive || slideAgentActive;
1255
1377
 
1256
1378
  // Comments for the current slide (for badge count)
1257
- const currentSlideCommentsQuery = useSlideComments(id ?? null, activeSlideId);
1379
+ const currentSlideCommentsQuery = useSlideComments(
1380
+ deck ? (id ?? null) : null,
1381
+ activeSlideId,
1382
+ );
1258
1383
  const currentSlideThreads: CommentThread[] =
1259
1384
  currentSlideCommentsQuery.data ?? [];
1260
1385
  const unresolvedCommentCount = currentSlideThreads.filter(
@@ -1269,6 +1394,11 @@ export default function DeckEditor() {
1269
1394
  accessCheckKey: currentDeckAccessKey,
1270
1395
  checkedAccessKey: checkedDeckAccessKey,
1271
1396
  retrying: retryingMissingDeck,
1397
+ privateDeckAccessConfirmed: Boolean(
1398
+ deckAccessStatus?.exists &&
1399
+ !deckAccessStatus.hasAccess &&
1400
+ deckAccessStatus.visibility === "private",
1401
+ ),
1272
1402
  })
1273
1403
  ) {
1274
1404
  return <DeckEditorSkeleton label={t("deckEditor.lookingForDeck")} />;
@@ -1276,10 +1406,20 @@ export default function DeckEditor() {
1276
1406
  if (!deck || !id) {
1277
1407
  return (
1278
1408
  <MissingDeckAccessPane
1409
+ accessStatus={deckAccessStatus}
1410
+ accessStatusError={deckAccessStatusQuery.isError}
1411
+ accessStatusLoading={loading || deckAccessStatusQuery.isLoading}
1279
1412
  hasTeamJoinOption={hasTeamJoinOption}
1280
1413
  orgLoading={orgLoading}
1281
1414
  orgError={orgError || loadError}
1415
+ requestAccessPending={requestDeckAccessMutation.isPending}
1416
+ accessRequestSent={accessRequestSentDeckId === id}
1417
+ accessRequestNotified={accessRequestNotified}
1418
+ signedIn={Boolean(session) && !sessionLoading}
1419
+ viewerEmail={session?.email ?? deckAccessStatus?.viewerEmail ?? null}
1282
1420
  refreshing={retryingMissingDeck}
1421
+ onRequestAccess={requestDeckAccess}
1422
+ onSignIn={openSignIn}
1283
1423
  onRetry={() => void retryOpenDeck()}
1284
1424
  onBack={() => navigate("/")}
1285
1425
  />
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Private deck links now explain access and let viewers notify the owner
@@ -83,3 +83,13 @@ export const slideComments = table("slide_comments", {
83
83
  createdAt: text("created_at").notNull().default(now()),
84
84
  updatedAt: text("updated_at").notNull().default(now()),
85
85
  });
86
+
87
+ export const deckEvents = table("deck_events", {
88
+ id: text("id").primaryKey(),
89
+ deckId: text("deck_id").notNull(),
90
+ type: text("type").notNull(),
91
+ message: text("message").notNull(),
92
+ payload: text("payload"),
93
+ createdBy: text("created_by").notNull().default("human"),
94
+ createdAt: text("created_at").notNull().default(now()),
95
+ });
@@ -235,6 +235,23 @@ export const runSlidesMigrations = runMigrations(
235
235
  name: "slides-share-design-system-snapshot",
236
236
  sql: `ALTER TABLE deck_share_links ADD COLUMN IF NOT EXISTS design_system_data TEXT`,
237
237
  },
238
+ // v22: durable access-request events for private deck links. The request
239
+ // action also sends the owner an email when outbound email is configured,
240
+ // but the event remains the source of truth when it is not.
241
+ {
242
+ version: 22,
243
+ name: "slides-deck-access-requests",
244
+ sql: `CREATE TABLE IF NOT EXISTS deck_events (
245
+ id TEXT PRIMARY KEY,
246
+ deck_id TEXT NOT NULL,
247
+ type TEXT NOT NULL,
248
+ message TEXT NOT NULL,
249
+ payload TEXT,
250
+ created_by TEXT NOT NULL DEFAULT 'human',
251
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
252
+ );
253
+ CREATE INDEX IF NOT EXISTS deck_events_deck_created_idx ON deck_events (deck_id, created_at)`,
254
+ },
238
255
  ],
239
256
  { table: "slides_migrations" },
240
257
  );
@@ -21,6 +21,7 @@ declare global {
21
21
  workspaceRuntime?: boolean;
22
22
  sentryDsn?: string;
23
23
  sentryEnvironment?: string;
24
+ deploymentEnvironment?: string;
24
25
  /**
25
26
  * Public PostHog project key + host. Publishable and identical for every
26
27
  * visitor, so it ships inside the CDN-cached SSR shell alongside the
@@ -3,6 +3,7 @@ import { llmConnectionTrackingProperties, } from "../shared/llm-connection.js";
3
3
  import { toPostHogExceptionProperties } from "../tracking/posthog-exception.js";
4
4
  import { getAnalyticsClientPlatform } from "./analytics-platform.js";
5
5
  import { getOrCreateAnalyticsAnonymousId, getOrCreateAnalyticsSessionId, } from "./analytics-session.js";
6
+ import { injectedAgentNativeConfig } from "./app-config.js";
6
7
  export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
7
8
  import { fetchAgentEngineStatus, fetchAuthSessionStatus, } from "./client-status-requests.js";
8
9
  import { installErrorCapture, } from "./error-capture.js";
@@ -681,6 +682,16 @@ function getClientSentryDsn() {
681
682
  window.__AGENT_NATIVE_CONFIG__?.sentryDsn ||
682
683
  resolveClientSentryDsnFromKeyProject(env));
683
684
  }
685
+ function resolveClientDeploymentEnvironment() {
686
+ const env = import.meta.env ?? {};
687
+ return (window.__AGENT_NATIVE_CONFIG__?.deploymentEnvironment ||
688
+ injectedAgentNativeConfig().deployment?.environment ||
689
+ env.VITE_AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT ||
690
+ env.VITE_SENTRY_ENVIRONMENT ||
691
+ window.__AGENT_NATIVE_CONFIG__?.sentryEnvironment ||
692
+ env.MODE ||
693
+ "production");
694
+ }
684
695
  function captureWithSentry(module, error, context) {
685
696
  return module.withScope((scope) => {
686
697
  if (context.tags) {
@@ -721,10 +732,12 @@ function ensureSentry(loadWithoutDsn = false) {
721
732
  }
722
733
  module.init({
723
734
  dsn,
724
- environment: window.__AGENT_NATIVE_CONFIG__?.sentryEnvironment ||
725
- import.meta.env?.MODE ||
726
- "production",
735
+ environment: resolveClientDeploymentEnvironment(),
727
736
  beforeSend(event) {
737
+ event.tags = {
738
+ ...event.tags,
739
+ deployment_environment: resolveClientDeploymentEnvironment(),
740
+ };
728
741
  if (shouldDropBrowserSentryNoise(event)) {
729
742
  return null;
730
743
  }
@@ -756,6 +769,7 @@ function ensureSentry(loadWithoutDsn = false) {
756
769
  },
757
770
  });
758
771
  module.setTag("runtime", "browser");
772
+ module.setTag("deployment_environment", resolveClientDeploymentEnvironment());
759
773
  _sentryInitialized = true;
760
774
  // Flush any user/tag that was set before init.
761
775
  if (_pendingSentryUser !== undefined) {
@@ -1169,8 +1183,7 @@ function maybeInstallErrorCapture(config) {
1169
1183
  send: sendExceptionEvent,
1170
1184
  getSessionContext: errorCaptureSessionContext,
1171
1185
  emitReplayEvent: emitExceptionToReplay,
1172
- environment: options.environment ||
1173
- import.meta.env?.MODE,
1186
+ environment: options.environment || resolveClientDeploymentEnvironment(),
1174
1187
  ...(options.release ? { release: options.release } : {}),
1175
1188
  ...(options.captureGlobalErrors !== undefined
1176
1189
  ? { captureGlobalErrors: options.captureGlobalErrors }
@@ -1410,6 +1423,7 @@ function resolveProps(name, params) {
1410
1423
  }
1411
1424
  const llmProps = llmConnectionTrackingProperties(_llmConnectionStatus);
1412
1425
  const enriched = { ...withTemplate };
1426
+ enriched.deployment_environment = resolveClientDeploymentEnvironment();
1413
1427
  for (const [key, value] of Object.entries(llmProps)) {
1414
1428
  if (enriched[key] === undefined)
1415
1429
  enriched[key] = value;