From 1c4bed6f915db6961357c5a038f106db2fbd51ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 15:38:44 +0000 Subject: [PATCH] feat(app-shell): the help menu's About names the console build (objectui#12970) The help menu on every console bar gains an About entry. Its dialog names the build the console was compiled from: VITE_APP_VERSION from the build's environment, or the short sha of HEAD the console's Vite config reads from git when that is unset. With neither, it states "Unknown build" in words. One reader, buildIdentity.ts, serves both the dialog and Sentry's release fallback. Text through new help.about* keys in all ten packs. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB --- .changeset/12970-about-build.md | 9 + apps/console/vite.config.ts | 23 ++ .../src/__tests__/buildIdentity-12970.test.ts | 64 +++++ packages/app-shell/src/buildIdentity.ts | 49 ++++ packages/app-shell/src/layout/AboutDialog.tsx | 62 +++++ packages/app-shell/src/layout/AppHeader.tsx | 17 ++ .../AppHeader.aboutBuild-12970.test.tsx | 256 ++++++++++++++++++ .../app-shell/src/observability/sentry.ts | 22 +- packages/i18n/src/locales/ar.ts | 5 + packages/i18n/src/locales/de.ts | 5 + packages/i18n/src/locales/en.ts | 5 + packages/i18n/src/locales/es.ts | 5 + packages/i18n/src/locales/fr.ts | 5 + packages/i18n/src/locales/ja.ts | 5 + packages/i18n/src/locales/ko.ts | 5 + packages/i18n/src/locales/pt.ts | 5 + packages/i18n/src/locales/ru.ts | 5 + packages/i18n/src/locales/zh.ts | 5 + 18 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 .changeset/12970-about-build.md create mode 100644 packages/app-shell/src/__tests__/buildIdentity-12970.test.ts create mode 100644 packages/app-shell/src/buildIdentity.ts create mode 100644 packages/app-shell/src/layout/AboutDialog.tsx create mode 100644 packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx diff --git a/.changeset/12970-about-build.md b/.changeset/12970-about-build.md new file mode 100644 index 0000000000..4b83d2350c --- /dev/null +++ b/.changeset/12970-about-build.md @@ -0,0 +1,9 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +'@object-ui/console': patch +--- + +The help menu gains an About entry on every console bar (the tenant console and the control plane draw the same header). It opens a small dialog naming the build the console was compiled from: `VITE_APP_VERSION` from the build's environment, or, when that is unset, the short sha of `HEAD` that the console's Vite config reads from git at build time. A console built with neither says "Unknown build" in words instead of showing an empty field. + +The dialog and Sentry's release fallback read the same value through one reader, so the two can no longer name different builds. The text comes from new `help.about*` keys in all ten language packs. diff --git a/apps/console/vite.config.ts b/apps/console/vite.config.ts index 885f088070..da8b4de797 100644 --- a/apps/console/vite.config.ts +++ b/apps/console/vite.config.ts @@ -8,6 +8,7 @@ import react from '@vitejs/plugin-react'; import path from 'path'; import fs from 'fs'; import zlib from 'node:zlib'; +import { execFileSync } from 'node:child_process'; // Relative specifiers carry their real file extension, and `__dirname` is // spelled `import.meta.dirname` throughout this file, so the config stays // loadable by Vite's `configLoader: 'native'` — which imports this file with @@ -587,6 +588,27 @@ function serveRuntimeAssets(): Plugin { // stable across HTML revisions. const basePath = process.env.VITE_BASE_PATH || './'; +// The console's build identity (objectui#12970): `VITE_APP_VERSION` from the +// build's environment when it is set (a CI or Docker build passes the objectui +// commit), else the short sha of `HEAD` read from git right here, so a local +// build names its commit too. Baked as `import.meta.env.VITE_APP_VERSION` — the +// one spelling `@object-ui/app-shell`'s `buildIdentity.ts` reads for both the +// help menu's About dialog and Sentry's release fallback. With neither (a build +// context without `.git`), nothing is baked and the dialog says the build is +// unknown. `rev-parse` answers in a shallow clone and on a detached HEAD alike. +function readGitShortSha(): string { + try { + return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { + cwd: import.meta.dirname, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +} +const consoleBuild = process.env.VITE_APP_VERSION?.trim() || readGitShortSha(); + // On Vercel/CI we skip the compression and visualizer plugins because the // Vercel CDN handles gzip/brotli automatically and bundle analysis is not // needed during CI builds. This reduces peak memory by ~1.5 GB. @@ -832,6 +854,7 @@ export default defineConfig({ 'process.env': {}, 'process.platform': '"browser"', 'process.version': '"0.0.0"', + ...(consoleBuild ? { 'import.meta.env.VITE_APP_VERSION': JSON.stringify(consoleBuild) } : {}), }, plugins: [ diff --git a/packages/app-shell/src/__tests__/buildIdentity-12970.test.ts b/packages/app-shell/src/__tests__/buildIdentity-12970.test.ts new file mode 100644 index 0000000000..c6607116ad --- /dev/null +++ b/packages/app-shell/src/__tests__/buildIdentity-12970.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The console's build identity is ONE read (objectui#12970): the help menu's + * About dialog and Sentry's release fallback both go through + * `resolveConsoleBuild`, so the two can never name different builds. + * + * Pinned through the pure readers because this repo's Vitest setup cannot put + * a `VITE_*` value on `import.meta.env` (the same reason `resolveSentryGate` + * is pure; see its header in `observability/sentry.ts`). + */ + +import { describe, it, expect } from 'vitest'; +import { resolveConsoleBuild, getConsoleBuild } from '../buildIdentity'; +import { resolveSentryRelease } from '../observability/sentry'; + +describe('resolveConsoleBuild: the console build identity (objectui#12970)', () => { + it('names the commit the build baked into VITE_APP_VERSION', () => { + expect(resolveConsoleBuild({ VITE_APP_VERSION: '0707b4ae' })).toBe('0707b4ae'); + }); + + it('trims surrounding whitespace from the baked value', () => { + expect(resolveConsoleBuild({ VITE_APP_VERSION: ' 0707b4ae\n' })).toBe('0707b4ae'); + }); + + it.each<[string, unknown]>([ + ['absent', undefined], + ['empty', ''], + ['whitespace only', ' '], + ['not a string', 42], + ])('answers null when the value is %s, so the dialog states the fallback', (_label, value) => { + expect(resolveConsoleBuild({ VITE_APP_VERSION: value })).toBeNull(); + }); + + it('answers null for a missing env object', () => { + expect(resolveConsoleBuild(undefined)).toBeNull(); + expect(resolveConsoleBuild(null)).toBeNull(); + }); + + it('reads null under this test setup, which bakes no VITE_APP_VERSION', () => { + expect(getConsoleBuild()).toBeNull(); + }); +}); + +describe('resolveSentryRelease: falls back to the same build identity (objectui#12970)', () => { + it('prefers an explicit VITE_SENTRY_RELEASE', () => { + expect(resolveSentryRelease({ VITE_SENTRY_RELEASE: 'rel-1', VITE_APP_VERSION: '0707b4ae' })).toBe('rel-1'); + }); + + it('falls back to the value resolveConsoleBuild reads, not a second spelling', () => { + for (const env of [ + { VITE_APP_VERSION: '0707b4ae' }, + { VITE_APP_VERSION: ' 0707b4ae ' }, + { VITE_SENTRY_RELEASE: '', VITE_APP_VERSION: 'be076dad' }, + ]) { + expect(resolveSentryRelease(env)).toBe(resolveConsoleBuild(env)); + } + }); + + it("answers 'unknown' when the build named neither", () => { + expect(resolveSentryRelease({})).toBe('unknown'); + expect(resolveSentryRelease({ VITE_SENTRY_RELEASE: ' ', VITE_APP_VERSION: '' })).toBe('unknown'); + }); +}); diff --git a/packages/app-shell/src/buildIdentity.ts b/packages/app-shell/src/buildIdentity.ts new file mode 100644 index 0000000000..2d359e0ece --- /dev/null +++ b/packages/app-shell/src/buildIdentity.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The console's own build identity — which objectui commit this bundle was + * compiled from (objectui#12970). + * + * ONE read, shared: the help menu's About dialog shows it, and Sentry tags its + * events with it when no `VITE_SENTRY_RELEASE` is set. There is no second + * spelling of the value anywhere in the shell. + * + * Where the value comes from — a property of the BUILD, never of the runtime + * (a server cannot know which console bundle it is serving): + * + * - `VITE_APP_VERSION` in the environment of the build that compiles the app. + * This package ships `tsc` output, so its `dist/` still reads + * `import.meta.env` and the APP's Vite build replaces it. + * - The console app's Vite config (`apps/console/vite.config.ts`) bakes the + * variable when it is set, and otherwise the short sha of `HEAD` read from + * git at build time, so a local build names its commit too. + * - Built with neither, the value is absent and this module answers `null`; + * the dialog then says so in words rather than showing an empty field. + * + * @module + */ + +/** The slice of `import.meta.env` this module reads. */ +export interface BuildIdentityEnv { + readonly VITE_APP_VERSION?: unknown; +} + +/** + * The build identity carried by `env`, or `null` when the build named none. + * + * A pure function of its input, split out from {@link getConsoleBuild} for the + * same reason `resolveSentryGate` is: this repo's Vitest setup cannot put a + * `VITE_*` value on `import.meta.env`, so the reading is pinned through this. + * Only a non-empty string counts; whitespace is trimmed. + */ +export function resolveConsoleBuild(env: BuildIdentityEnv | null | undefined): string | null { + const raw = env?.VITE_APP_VERSION; + if (typeof raw !== 'string') return null; + const value = raw.trim(); + return value === '' ? null : value; +} + +/** The build identity this bundle was compiled with, or `null`. */ +export function getConsoleBuild(): string | null { + return resolveConsoleBuild((import.meta as { env?: BuildIdentityEnv }).env); +} diff --git a/packages/app-shell/src/layout/AboutDialog.tsx b/packages/app-shell/src/layout/AboutDialog.tsx new file mode 100644 index 0000000000..0055502177 --- /dev/null +++ b/packages/app-shell/src/layout/AboutDialog.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * AboutDialog — the help menu's About entry (objectui#12970). + * + * Names the build this console was compiled from, so a reviewer can tell which + * objectui build a deployment serves. The value is the shared build identity + * (`buildIdentity.ts`), the same read Sentry's release falls back to. A console + * built without one says so in words; the field is never left empty. + * + * The runtime's own build identity is deliberately not shown here: the shell + * has no request that reads it today, and adding one is outside this dialog. + * @module + */ + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@object-ui/components'; +import { useObjectTranslation } from '@object-ui/i18n'; + +export interface AboutDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** The console's build identity, or `null` when the build named none. */ + build: string | null; +} + +export function AboutDialog({ open, onOpenChange, build }: AboutDialogProps) { + const { t } = useObjectTranslation(); + return ( + + + + {t('help.aboutTitle', { defaultValue: 'About this console' })} + + {t('help.aboutDescription', { defaultValue: 'The build this console was compiled from.' })} + + +
+
+ {t('help.aboutBuild', { defaultValue: 'Console build' })} +
+
+ {build ? ( + {build} + ) : ( + + {t('help.aboutBuildUnknown', { + defaultValue: 'Unknown build (no version was set when this console was built)', + })} + + )} +
+
+
+
+ ); +} diff --git a/packages/app-shell/src/layout/AppHeader.tsx b/packages/app-shell/src/layout/AppHeader.tsx index 4dbce5f187..b1af036b0a 100644 --- a/packages/app-shell/src/layout/AppHeader.tsx +++ b/packages/app-shell/src/layout/AppHeader.tsx @@ -69,6 +69,7 @@ import { Keyboard, Pencil, MoreHorizontal, + Info, } from 'lucide-react'; import { useState, useEffect, useLayoutEffect, useCallback, useRef, type RefObject } from 'react'; @@ -107,6 +108,8 @@ import { resolveViewId } from '../utils/resolveViewId.js'; import { getProductName, getLogoUrl } from '../runtime-config.js'; import { LocalizedSidebarTrigger } from './LocalizedSidebarTrigger.js'; import { PreviewBadge } from './PreviewBadge.js'; +import { AboutDialog } from './AboutDialog.js'; +import { getConsoleBuild } from '../buildIdentity.js'; import { GO_HOME_SHORTCUT, useAdvertiseShortcut, type ShortcutChord } from '../chrome/advertisedShortcuts.js'; function humanizeSlug(slug: string): string { @@ -443,6 +446,9 @@ export function AppHeader({ // Click-reachable entry for the keyboard-shortcuts dialog (was `?`-key only). // Shares the `?shortcuts=1` URL param with KeyboardShortcutsDialog (C2/C3). const { openOverlay: openShortcuts } = useUrlOverlay(KEYBOARD_SHORTCUTS_PARAM); + // The help menu's About dialog (objectui#12970): ephemeral, so component + // state, not a URL param — nobody shares a link to it. + const [aboutOpen, setAboutOpen] = useState(false); const { user, signOut, @@ -1137,8 +1143,19 @@ export function AppHeader({ {t('help.onlineDocs', { defaultValue: 'Online documentation' })} + {/* About — names the console's own build, on every variant + (objectui#12970). */} + setAboutOpen(true)} + > + + {t('help.about', { defaultValue: 'About' })} + + {/* Group 3: Account (theme + lang moved into avatar dropdown) */} diff --git a/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx b/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx new file mode 100644 index 0000000000..0544a15bac --- /dev/null +++ b/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx @@ -0,0 +1,256 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The help menu's About entry (objectui#12970): on every `AppHeader` variant — + * the tenant console's `app` bar and the control plane's `home` / `orgs` / + * `studio` bars are the one component — the help menu carries 「关于」 / About, + * and the dialog it opens names the build this console was compiled from. + * + * The build identity is `buildIdentity.ts`'s one read, stubbed here through a + * mutable reading: this repo's Vitest setup cannot put a `VITE_*` value on + * `import.meta.env`, and the pure reader is pinned on its own in + * `buildIdentity-12970.test.ts`. + * + * `t` answers `«key»` and drops `defaultValue`, so a label written as a + * literal cannot pass for one read from the packs. + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, within } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; + +vi.mock('@object-ui/i18n', async (importOriginal) => ({ + ...(await importOriginal>()), + useObjectTranslation: () => ({ + language: 'en', + t: (key: string) => `«${key}»`, + }), + useObjectLabel: () => { + const label = (x: { name?: string; label?: string } | string) => + typeof x === 'string' ? x : String(x?.label ?? x?.name ?? ''); + return { + objectLabel: label, + objectPluralLabel: label, + dashboardLabel: label, + pageLabel: label, + reportLabel: label, + viewLabel: (_o: string, _v: string, fallback?: string) => fallback ?? '', + appLabel: label, + }; + }, +})); + +/** + * Passthrough menu primitives: Radix keeps a closed menu unmounted, so the + * help menu's CONTENT renders its children here. The dialog is the real one. + */ +vi.mock('@object-ui/components', async (importOriginal) => { + const actual = await importOriginal>(); + const strip = (p: any) => { + const { asChild, variant, size, align, sideOffset, ...rest } = p ?? {}; + return rest; + }; + const Pass = ({ children, ...p }: any) =>
{children}
; + return { + ...actual, + Button: ({ children, asChild, variant, size, ...p }: any) => + asChild ? {children} : , + DropdownMenu: ({ children }: any) =>
{children}
, + DropdownMenuTrigger: Pass, + DropdownMenuContent: Pass, + DropdownMenuItem: ({ children, onClick, asChild, ...p }: any) => ( +
{children}
+ ), + DropdownMenuLabel: Pass, + DropdownMenuSeparator: () => null, + DropdownMenuGroup: Pass, + Avatar: Pass, + AvatarImage: () => null, + AvatarFallback: Pass, + Popover: Pass, + PopoverTrigger: Pass, + PopoverContent: () => null, + cn: (...c: any[]) => c.filter(Boolean).join(' '), + }; +}); + +vi.mock('lucide-react', () => { + const Icon = () => ; + return new Proxy({ __esModule: true } as Record, { + get: (target, prop) => { + if (prop === 'then' || prop === '__esModule' || typeof prop === 'symbol') return target[prop]; + return Icon; + }, + has: (_target, prop) => prop !== 'then', + }); +}); + +vi.mock('@object-ui/react', async (importOriginal) => ({ + ...(await importOriginal>()), + useOffline: () => ({ isOnline: true }), +})); + +vi.mock('@object-ui/collaboration', async (importOriginal) => ({ + ...(await importOriginal>()), + PresenceAvatars: () => null, + useTenantPresence: () => [], +})); + +vi.mock('../ModeToggle', () => ({ ModeToggle: () => null })); +vi.mock('../LocaleSwitcher', () => ({ LocaleSwitcher: () => null })); +vi.mock('../NotificationPreferencesMenu', () => ({ NotificationPreferencesMenu: () => null })); +vi.mock('../WorkspaceSwitcher', () => ({ WorkspaceSwitcher: () => null })); +vi.mock('../CurrentOrganizationIndicator', () => ({ CurrentOrganizationIndicator: () => null })); +vi.mock('../ConnectionStatus', () => ({ ConnectionStatus: () => null })); +vi.mock('../AppSwitcher', () => ({ AppSwitcher: () => null })); +vi.mock('../LocalizedSidebarTrigger', () => ({ LocalizedSidebarTrigger: () => null })); +vi.mock('../PreviewBadge', () => ({ PreviewBadge: () => null })); +vi.mock('../InboxPopover', () => ({ InboxPopover: () => null })); +vi.mock('../../hooks/useAiSurface', () => ({ + useAiSurfaceEnabled: () => ({ enabled: false, isLoading: false }), +})); +vi.mock('../../context/NavigationContext.js', () => ({ + useNavigationContext: () => ({ currentAppName: 'crm', recordTitle: undefined }), +})); + +/** The build identity the header reads; each case sets it. */ +let consoleBuild: string | null = null; + +vi.mock('../../buildIdentity', async (importOriginal) => ({ + ...(await importOriginal>()), + getConsoleBuild: () => consoleBuild, +})); + +/** One stable identity for the whole file: AppHeader keys an effect on it. */ +const getAuthConfig = () => Promise.resolve({ features: { multiOrgEnabled: true } }); + +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useAuth: () => ({ + user: { id: 'u1', name: 'Lin Yue', email: 'linyue@example.com' }, + signOut: vi.fn(), + isAuthEnabled: true, + organizations: [], + activeOrganization: null, + isOrganizationsLoading: false, + getAuthConfig, + }), + getUserInitials: () => 'LY', + useWorkspaceAdminStatus: () => ({ isAdmin: false, isResolved: true }), +})); + +vi.mock('../../providers/MetadataProvider', () => ({ + useMetadata: () => ({ + apps: [{ name: 'crm', label: 'CRM', _packageId: 'com.example.crm' }], + dashboards: [], + pages: [], + reports: [], + }), +})); + +/** Stable identity: the header's readers key their effects on the adapter. */ +const fakeAdapter = { find: () => Promise.resolve({ data: [] }), getClient: () => undefined }; + +vi.mock('../../providers/AdapterProvider', () => ({ + useAdapter: () => fakeAdapter, +})); + +import { AppHeader, type AppHeaderVariant } from '../AppHeader'; + +const MEMBER: MePermissionsResponse = { + authenticated: true, + userId: 'u1', + tenantId: 'org_1', + roles: [], + permissionSets: [], + systemPermissions: [], + objects: {}, + fields: {}, +}; + +const PATHS: Record = { + app: '/apps/crm', + home: '/home', + orgs: '/organizations', + studio: '/studio', +}; + +function renderHeader(variant: AppHeaderVariant) { + return render( + + + + + ) : ( + + ) + } + /> + + + , + ); +} + +function openAbout() { + fireEvent.click(screen.getByTestId('action:about:open')); + return screen.getByTestId('overlay:about'); +} + +beforeEach(() => { + consoleBuild = null; + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ data: [] }) }))); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('AppHeader help menu: About names the console build (objectui#12970)', () => { + it.each(['app', 'home', 'orgs', 'studio'])( + 'the `%s` bar\'s help menu carries About, read from the packs', + (variant) => { + renderHeader(variant); + + const item = screen.getByTestId('action:about:open'); + expect(item).toHaveAttribute('role', 'menuitem'); + expect(item).toHaveTextContent('«help.about»'); + // The dialog is closed until the item is chosen. + expect(screen.queryByTestId('overlay:about')).not.toBeInTheDocument(); + }, + ); + + it.each(['app', 'home'])( + 'on the `%s` bar the dialog names the commit the build baked in', + (variant) => { + consoleBuild = '0707b4ae'; + renderHeader(variant); + + const dialog = openAbout(); + expect(within(dialog).getByText('«help.aboutTitle»')).toBeInTheDocument(); + expect(within(dialog).getByText('«help.aboutBuild»')).toBeInTheDocument(); + const value = within(dialog).getByTestId('about:console-build'); + expect(value).toHaveTextContent('0707b4ae'); + expect(within(dialog).queryByText('«help.aboutBuildUnknown»')).not.toBeInTheDocument(); + }, + ); + + it.each(['app', 'home'])( + 'on the `%s` bar a build with no identity states the fallback, never an empty field', + (variant) => { + consoleBuild = null; + renderHeader(variant); + + const value = within(openAbout()).getByTestId('about:console-build'); + expect(value).toHaveTextContent('«help.aboutBuildUnknown»'); + expect(value.textContent?.trim()).not.toBe(''); + }, + ); +}); diff --git a/packages/app-shell/src/observability/sentry.ts b/packages/app-shell/src/observability/sentry.ts index 36a553bd38..858798af74 100644 --- a/packages/app-shell/src/observability/sentry.ts +++ b/packages/app-shell/src/observability/sentry.ts @@ -62,12 +62,14 @@ * - `VITE_SENTRY_RELEASE` — which bundle produced a stack trace. It must match * the source maps that bundle's pipeline uploaded, and a server cannot know * which Console build it is serving, so this one stays build-time. Falls - * back to `VITE_APP_VERSION`, then `'unknown'`. + * back to the console's build identity (`buildIdentity.ts`: `VITE_APP_VERSION`, + * or the git sha the console's Vite config baked), then `'unknown'`. * * @module */ import { getClientErrorReporting, type RuntimeClientErrorReporting } from '../runtime-config.js'; +import { resolveConsoleBuild, type BuildIdentityEnv } from '../buildIdentity.js'; type SentryModule = typeof import('@sentry/react'); @@ -172,6 +174,22 @@ function rate(value: unknown, fallback: number): number { : fallback; } +/** The slice of `import.meta.env` {@link resolveSentryRelease} reads. */ +export interface SentryReleaseEnv extends BuildIdentityEnv { + readonly VITE_SENTRY_RELEASE?: unknown; +} + +/** + * The release Sentry tags events with: `VITE_SENTRY_RELEASE` when the build set + * one, else the console's build identity — read through `resolveConsoleBuild`, + * the same one read the help menu's About dialog shows (objectui#12970) — else + * `'unknown'`. + */ +export function resolveSentryRelease(env: SentryReleaseEnv): string { + const explicit = typeof env.VITE_SENTRY_RELEASE === 'string' ? env.VITE_SENTRY_RELEASE.trim() : ''; + return explicit || resolveConsoleBuild(env) || 'unknown'; +} + /** * Returns the loaded Sentry module, or `null` if Sentry was never initialized * (e.g. the runtime served no DSN). Callers must handle the null case. @@ -216,7 +234,7 @@ export function initSentry(): Promise { // a release identifies which bundle produced a stack trace and has to // match the source maps that bundle's pipeline uploaded, which no // server can know. - release: env.VITE_SENTRY_RELEASE || env.VITE_APP_VERSION || 'unknown', + release: resolveSentryRelease(env), tracesSampleRate: gate.tracesSampleRate, // IP address + User-Agent — decided by `resolveSentryGate` above, and // OPT-IN there. Defaulting it on is how PII left an air-gapped network diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 519ded9f0a..ed855b0559 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -2831,6 +2831,11 @@ const ar = { allDocs: 'كل الوثائق', onlineDocs: 'الوثائق عبر الإنترنت', loadingDocumentation: 'جارٍ تحميل الوثائق', + about: 'حول', + aboutTitle: 'حول وحدة التحكم هذه', + aboutDescription: 'الإصدار الذي بُنيت منه وحدة التحكم هذه.', + aboutBuild: 'إصدار وحدة التحكم', + aboutBuildUnknown: 'إصدار غير معروف (لم يُحدَّد أي إصدار عند بناء وحدة التحكم هذه)', }, sidebar: { packageManagement: "إدارة الحزم", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index d91ec25d49..976a480c09 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -2619,6 +2619,11 @@ const de = { allDocs: 'Gesamte Dokumentation', onlineDocs: 'Online-Dokumentation', loadingDocumentation: 'Dokumentation wird geladen', + about: 'Über', + aboutTitle: 'Über diese Konsole', + aboutDescription: 'Der Build, aus dem diese Konsole kompiliert wurde.', + aboutBuild: 'Konsolen-Build', + aboutBuildUnknown: 'Unbekannter Build (beim Erstellen dieser Konsole wurde keine Version gesetzt)', }, sidebar: { packageManagement: "Paketverwaltung", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index aa219bb65c..ff101a5fa3 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -3150,6 +3150,11 @@ const en = { keyboardShortcuts: 'Keyboard shortcuts', onlineDocs: 'Online documentation', loadingDocumentation: 'Loading documentation', + about: 'About', + aboutTitle: 'About this console', + aboutDescription: 'The build this console was compiled from.', + aboutBuild: 'Console build', + aboutBuildUnknown: 'Unknown build (no version was set when this console was built)', }, sidebar: { settings: 'Settings', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index dabe9bd9cb..6f95b6261c 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -2671,6 +2671,11 @@ const es = { allDocs: 'Toda la documentación', onlineDocs: 'Documentación en línea', loadingDocumentation: 'Cargando documentación', + about: 'Acerca de', + aboutTitle: 'Acerca de esta consola', + aboutDescription: 'La compilación a partir de la cual se generó esta consola.', + aboutBuild: 'Compilación de la consola', + aboutBuildUnknown: 'Compilación desconocida (no se definió ninguna versión al compilar esta consola)', }, sidebar: { packageManagement: "Gestión de paquetes", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 094948b598..566a350758 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -2668,6 +2668,11 @@ const fr = { allDocs: 'Toute la documentation', onlineDocs: 'Documentation en ligne', loadingDocumentation: 'Chargement de la documentation', + about: 'À propos', + aboutTitle: 'À propos de cette console', + aboutDescription: 'Le build à partir duquel cette console a été compilée.', + aboutBuild: 'Build de la console', + aboutBuildUnknown: 'Build inconnu (aucune version n’a été définie lors de la compilation de cette console)', }, sidebar: { packageManagement: "Gestion des packages", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 3c5a9ff052..5a2a35ce65 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -2621,6 +2621,11 @@ const ja = { allDocs: 'すべてのドキュメント', onlineDocs: 'オンラインドキュメント', loadingDocumentation: 'ドキュメントを読み込み中', + about: 'バージョン情報', + aboutTitle: 'このコンソールについて', + aboutDescription: 'このコンソールのコンパイル元となったビルドです。', + aboutBuild: 'コンソールのビルド', + aboutBuildUnknown: '不明なビルド(このコンソールのビルド時にバージョンが設定されていません)', }, sidebar: { packageManagement: "パッケージ管理", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 31fd912b39..2a71ae941e 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -2619,6 +2619,11 @@ const ko = { allDocs: '전체 문서', onlineDocs: '온라인 문서', loadingDocumentation: '문서 로딩 중', + about: '정보', + aboutTitle: '이 콘솔 정보', + aboutDescription: '이 콘솔이 컴파일된 빌드입니다.', + aboutBuild: '콘솔 빌드', + aboutBuildUnknown: '알 수 없는 빌드(이 콘솔을 빌드할 때 버전이 설정되지 않음)', }, sidebar: { packageManagement: "패키지 관리", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 2a43fbb41b..badff71dc6 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -2666,6 +2666,11 @@ const pt = { allDocs: 'Toda a documentação', onlineDocs: 'Documentação online', loadingDocumentation: 'Carregando documentação', + about: 'Sobre', + aboutTitle: 'Sobre este console', + aboutDescription: 'A build a partir da qual este console foi compilado.', + aboutBuild: 'Build do console', + aboutBuildUnknown: 'Build desconhecida (nenhuma versão foi definida ao compilar este console)', }, sidebar: { packageManagement: "Gerenciamento de pacotes", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 61e8c66ae7..49d3392bc4 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -2735,6 +2735,11 @@ const ru = { allDocs: 'Вся документация', onlineDocs: 'Онлайн-документация', loadingDocumentation: 'Загрузка документации', + about: 'О программе', + aboutTitle: 'Об этой консоли', + aboutDescription: 'Сборка, из которой скомпилирована эта консоль.', + aboutBuild: 'Сборка консоли', + aboutBuildUnknown: 'Неизвестная сборка (при сборке этой консоли версия не была задана)', }, sidebar: { packageManagement: "Управление пакетами", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 4fced541eb..e4a3a68e07 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -2698,6 +2698,11 @@ const zh = { allDocs: '全部文档', onlineDocs: '在线文档', loadingDocumentation: '正在加载文档', + about: '关于', + aboutTitle: '关于此控制台', + aboutDescription: '此控制台编译所用的构建。', + aboutBuild: '控制台构建', + aboutBuildUnknown: '未知构建(构建此控制台时未设置版本)', }, sidebar: { settings: '设置',