From 880408e9dca1a583ae900271e95c5102bc9d1218 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Thu, 9 Jan 2025 15:50:30 +0700 Subject: [PATCH 01/12] feat(web): fetch env from API endpoint in client side --- apps/web/sentry.client.config.ts | 54 +++- apps/web/sentry.edge.config.ts | 23 +- apps/web/sentry.server.config.ts | 23 +- .../src/components/BlobscanVersionInfo.tsx | 34 +-- apps/web/src/components/ExplorerDetails.tsx | 13 +- .../src/components/Filters/RollupFilter.tsx | 34 +-- apps/web/src/components/Filters/index.tsx | 46 +++- apps/web/src/components/NavigationMenus.tsx | 22 +- .../src/components/SidebarNavigationMenu.tsx | 46 ++-- apps/web/src/components/content.tsx | 102 +++---- apps/web/src/env.mjs | 39 --- apps/web/src/pages/_app.tsx | 39 +-- apps/web/src/pages/block/[id].tsx | 253 +++++++++--------- apps/web/src/providers/Env.tsx | 69 +++++ apps/web/src/utils/env.ts | 10 + packages/api/src/app-router.ts | 2 + packages/api/src/getEnv.ts | 11 + packages/env/index.ts | 23 ++ 18 files changed, 517 insertions(+), 326 deletions(-) create mode 100644 apps/web/src/providers/Env.tsx create mode 100644 apps/web/src/utils/env.ts create mode 100644 packages/api/src/getEnv.ts diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index d268d406c..260ae45c1 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -4,11 +4,49 @@ import * as Sentry from "@sentry/nextjs"; -import { env } from "./src/env.mjs"; - -Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN_WEB, - environment: env.NEXT_PUBLIC_NETWORK_NAME, - tracesSampleRate: 1, - debug: false, -}); +// DISCUSS: on the first load of the app, there is no value, should we put a fallback? or retry the operation til there is a value in +// the local storage? After a first load, always works +const getEnvFromLocalStorage = (key: string) => { + if (typeof window !== "undefined") { + try { + const storedEnv = localStorage.getItem("env"); + if (storedEnv) { + const parsedEnv = JSON.parse(storedEnv) as Record; + return parsedEnv[`${key}`]; + } + } catch (error) { + console.error("Failed to read env from localStorage:", error); + } + } + return undefined; +}; + +const initSentry = () => { + const retryInterval = 1000; + const maxRetries = 10; + let retries = 0; + + const interval = setInterval(() => { + const dns = getEnvFromLocalStorage("PUBLIC_SENTRY_DSN_WEB"); + const environment = getEnvFromLocalStorage("PUBLIC_NETWORK_NAME"); + + if (environment || dns) { + Sentry.init({ + dsn: dns, + environment, + tracesSampleRate: 1, + debug: false, + }); + + clearInterval(interval); + } + + retries += 1; + if (retries >= maxRetries) { + clearInterval(interval); + console.warn("Failed to initialize Sentry after maximum retries."); + } + }, retryInterval); +}; + +initSentry(); diff --git a/apps/web/sentry.edge.config.ts b/apps/web/sentry.edge.config.ts index c552e322b..634ac2556 100644 --- a/apps/web/sentry.edge.config.ts +++ b/apps/web/sentry.edge.config.ts @@ -5,11 +5,18 @@ import * as Sentry from "@sentry/nextjs"; -import { env } from "./src/env.mjs"; - -Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN_WEB, - environment: env.NEXT_PUBLIC_NETWORK_NAME, - tracesSampleRate: 1, - debug: false, -}); +import { fetchEnv } from "~/utils/env"; + +const initSentry = async () => { + const res = await fetchEnv(); + const env = res?.result?.data?.json as Record; + + Sentry.init({ + dsn: env["NEXT_PUBLIC_SENTRY_DSN_WEB"], + environment: env["NEXT_PUBLIC_NETWORK_NAME"], + tracesSampleRate: 1, + debug: false, + }); +}; + +initSentry(); diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index 48eaca44b..95653b93b 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -1,14 +1,21 @@ // This file configures the initialization of Sentry on the server. // The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ +// https://docs.sentry.io/platforms/javasccript/guides/nextjs/ import * as Sentry from "@sentry/nextjs"; -import { env } from "./src/env.mjs"; +import { fetchEnv } from "~/utils/env"; -Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN_WEB, - environment: env.NEXT_PUBLIC_NETWORK_NAME, - tracesSampleRate: 1, - debug: false, -}); +const initSentry = async () => { + const res = await fetchEnv(); + const env = res?.result?.data?.json as Record; + + Sentry.init({ + dsn: env["NEXT_PUBLIC_SENTRY_DSN_WEB"], + environment: env["NEXT_PUBLIC_NETWORK_NAME"], + tracesSampleRate: 1, + debug: false, + }); +}; + +initSentry(); diff --git a/apps/web/src/components/BlobscanVersionInfo.tsx b/apps/web/src/components/BlobscanVersionInfo.tsx index ab9a63efd..4c09d8930 100644 --- a/apps/web/src/components/BlobscanVersionInfo.tsx +++ b/apps/web/src/components/BlobscanVersionInfo.tsx @@ -1,29 +1,21 @@ -import { env } from "~/env.mjs"; +import { useEnv } from "~/providers/Env"; import { Link } from "./Link"; -function getVersionData(): { url: string; label: string } { - if (env.NEXT_PUBLIC_BLOBSCAN_RELEASE) { - return { - url: `https://github.com/Blobscan/blobscan/releases/tag/${env.NEXT_PUBLIC_BLOBSCAN_RELEASE}`, - label: env.NEXT_PUBLIC_BLOBSCAN_RELEASE, - }; - } +export const BlobscanVersionInfo: React.FC = () => { + const { env } = useEnv(); - if (env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA) { - return { - url: `https://github.com/Blobscan/blobscan/commit/${env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA}`, - label: env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA.slice(0, 7), - }; - } + let url = "https://github.com/Blobscan/blobscan/"; + let label = "Development"; - return { - url: "https://github.com/Blobscan/blobscan/", - label: "Development", - }; -} + if (env["PUBLIC_BLOBSCAN_RELEASE"]) { + url = `https://github.com/Blobscan/blobscan/releases/tag/${env["PUBLIC_BLOBSCAN_RELEASE"]}`; + label = env["PUBLIC_BLOBSCAN_RELEASE"] as string; + } -export const BlobscanVersionInfo: React.FC = () => { - const { url, label } = getVersionData(); + if (env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]) { + url = `https://github.com/Blobscan/blobscan/commit/${env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]}`; + label = (env["PUBLIC_VERCEL_GIT_COMMIT_SHA"] as string).slice(0, 7); + } return (
diff --git a/apps/web/src/components/ExplorerDetails.tsx b/apps/web/src/components/ExplorerDetails.tsx index 3c0cd051c..0ab45195f 100644 --- a/apps/web/src/components/ExplorerDetails.tsx +++ b/apps/web/src/components/ExplorerDetails.tsx @@ -7,8 +7,8 @@ import Skeleton from "react-loading-skeleton"; import { formatTtl } from "@blobscan/dates"; import { api } from "~/api-client"; -import { env } from "~/env.mjs"; import Gas from "~/icons/gas.svg"; +import { useEnv } from "~/providers/Env"; import { capitalize, formatNumber } from "~/utils"; import { EtherUnitDisplay } from "./Displays/EtherUnitDisplay"; @@ -45,11 +45,20 @@ export function ExplorerDetails({ placement }: ExplorerDetailsProps) { const { data: blobStoragesState } = api.blobStoragesState.getState.useQuery(); const { data: latestBlock } = api.block.getLatestBlock.useQuery(); + const { env, isLoading: envLoading } = useEnv(); + const explorerDetailsItems: ExplorerDetailsItemProps[] = []; if (placement === "top") { explorerDetailsItems.push( - { name: "Network", value: capitalize(env.NEXT_PUBLIC_NETWORK_NAME) }, + { + name: "Network", + value: envLoading ? ( + + ) : ( + capitalize(env["PUBLIC_NETWORK_NAME"] as string) + ), + }, { name: "Blob gas price", icon: , diff --git a/apps/web/src/components/Filters/RollupFilter.tsx b/apps/web/src/components/Filters/RollupFilter.tsx index f22657a13..38e67c30c 100644 --- a/apps/web/src/components/Filters/RollupFilter.tsx +++ b/apps/web/src/components/Filters/RollupFilter.tsx @@ -1,44 +1,22 @@ import { useRef } from "react"; import type { FC } from "react"; -import { getChainRollups } from "@blobscan/rollups"; - import { Dropdown } from "~/components/Dropdown"; import type { DropdownProps, Option } from "~/components/Dropdown"; -import { RollupIcon } from "~/components/RollupIcon"; -import { env } from "~/env.mjs"; -import type { Rollup } from "~/types"; -import { capitalize, getChainIdByName } from "~/utils"; -import { RollupBadge } from "../Badges/RollupBadge"; -type RollupFilterProps = Pick & { +type RollupFilterProps = Pick< + DropdownProps, + "selected" | "disabled" | "options" +> & { onChange(newRollups: Option[]): void; selected: Option[] | null; }; -const chainId = getChainIdByName(env.NEXT_PUBLIC_NETWORK_NAME); -const rollups = chainId ? getChainRollups(chainId) : []; - -export const ROLLUP_OPTIONS = rollups.map( - ([name, addresses]) => - ({ - value: addresses, - selectedLabel: ( - - ), - label: ( -
- -
{capitalize(name)}
-
- ), - } satisfies Option) -) satisfies Option[]; - export const RollupFilter: FC = function ({ onChange, selected, disabled, + options, }) { const noneIsSelected = useRef(false); @@ -66,7 +44,7 @@ export const RollupFilter: FC = function ({ return ( { + const chainId = + !envLoading && getChainIdByName(env["PUBLIC_NETWORK_NAME"] as string); + const rollups = chainId ? getChainRollups(chainId) : []; + + return rollups.map( + ([name, addresses]) => + ({ + value: addresses, + selectedLabel: ( + + ), + label: ( +
+ +
{capitalize(name)}
+
+ ), + } satisfies Option) + ); + }, [env, envLoading]); + useEffect(() => { const { sort } = queryParams.paginationParams; const { @@ -177,7 +206,7 @@ export const Filters: FC = function () { const newFilters: Partial = {}; if (from) { - const rollupOptions = ROLLUP_OPTIONS.filter((opt) => { + const rollupOptions_ = rollupOptions.filter((opt) => { const fromAddresses = from?.split(FROM_ADDRESSES_FORMAT_SEPARATOR); const rollupOptionAddresses = Array.isArray(opt.value) ? opt.value @@ -185,13 +214,13 @@ export const Filters: FC = function () { return ( rollupOptionAddresses.filter((rollupAddress) => - fromAddresses?.includes(rollupAddress) + fromAddresses?.includes(rollupAddress as string) ).length > 0 ); }); - if (rollupOptions) { - newFilters.rollups = rollupOptions; + if (rollupOptions_) { + newFilters.rollups = rollupOptions_; } } @@ -225,7 +254,7 @@ export const Filters: FC = function () { } dispatch({ type: "UPDATE", payload: newFilters }); - }, [queryParams]); + }, [queryParams, rollupOptions]); return ( @@ -265,6 +294,7 @@ export const Filters: FC = function () {
{ + const { env, isLoading: envLoading } = useEnv(); + + const navigationItems = useMemo(() => { + const networkName = env["PUBLIC_NETWORK_NAME"] as string; + const publicSupportedNetworks = env["PUBLIC_SUPPORTED_NETWORKS"] as string; + return !envLoading + ? getNavigationItems(networkName, publicSupportedNetworks) + : undefined; + }, [envLoading, env]); + + if (!navigationItems) { + return ; + } + return (
- {NAVIGATION_ITEMS.map((item) => + {navigationItems.map((item) => isExpandibleNavigationItem(item) ? ( ) : ( diff --git a/apps/web/src/components/SidebarNavigationMenu.tsx b/apps/web/src/components/SidebarNavigationMenu.tsx index cdf24e0d3..8e699f6f3 100644 --- a/apps/web/src/components/SidebarNavigationMenu.tsx +++ b/apps/web/src/components/SidebarNavigationMenu.tsx @@ -1,10 +1,12 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { Bars3Icon } from "@heroicons/react/24/solid"; import cn from "classnames"; +import Skeleton from "react-loading-skeleton"; +import { useEnv } from "~/providers/Env"; import { BlobscanLogo } from "./BlobscanLogo"; import { Collapsable } from "./Collapsable"; import { IconButton } from "./IconButton"; @@ -12,7 +14,7 @@ import { Rotable } from "./Rotable"; import { SidePanel, useSidePanel } from "./SidePanel"; import { ThemeModeButton } from "./ThemeModeButton"; import type { ExpandibleNavigationItem, NavigationItem } from "./content"; -import { isExpandibleNavigationItem, NAVIGATION_ITEMS } from "./content"; +import { isExpandibleNavigationItem, getNavigationItems } from "./content"; export function SidebarNavigationMenu({ className }: { className?: string }) { const [open, setOpen] = useState(false); @@ -21,6 +23,16 @@ export function SidebarNavigationMenu({ className }: { className?: string }) { const closeSidebar = useCallback(() => setOpen(false), []); + const { env, isLoading: envLoading } = useEnv(); + + const navigationItems = useMemo(() => { + const networkName = env["PUBLIC_NETWORK_NAME"] as string; + const publicSupportedNetworks = env["PUBLIC_SUPPORTED_NETWORKS"] as string; + return !envLoading + ? getNavigationItems(networkName, publicSupportedNetworks) + : undefined; + }, [envLoading, env]); + return (
@@ -30,20 +42,24 @@ export function SidebarNavigationMenu({ className }: { className?: string }) {
- {NAVIGATION_ITEMS.map((item, i) => - isExpandibleNavigationItem(item) ? ( - - ) : ( - + {navigationItems ? ( + navigationItems.map((item, i) => + isExpandibleNavigationItem(item) ? ( + + ) : ( + + ) ) + ) : ( + )}
diff --git a/apps/web/src/components/content.tsx b/apps/web/src/components/content.tsx index e7de44735..10333e27e 100644 --- a/apps/web/src/components/content.tsx +++ b/apps/web/src/components/content.tsx @@ -6,7 +6,6 @@ import { Squares2X2Icon, } from "@heroicons/react/24/solid"; -import { env } from "~/env.mjs"; import EthereumIcon from "~/icons/ethereum.svg"; import { buildBlocksRoute, @@ -15,17 +14,15 @@ import { buildAllStatsRoute, } from "~/utils"; -function resolveApiUrl(): string { - if (env.NEXT_PUBLIC_NETWORK_NAME === "mainnet") { +function resolveApiUrl(networkName: string): string { + if (networkName === "mainnet") { return "https://api.blobscan.com"; } - return `https://api.${env.NEXT_PUBLIC_NETWORK_NAME}.blobscan.com`; + return `https://api.${networkName}.blobscan.com`; } -type Network = typeof env.NEXT_PUBLIC_NETWORK_NAME; - -const NETWORKS_FIRST_BLOB_NUMBER: Record = { +const NETWORKS_FIRST_BLOB_NUMBER: Record = { mainnet: 19426589, holesky: 894735, sepolia: 5187052, @@ -34,8 +31,8 @@ const NETWORKS_FIRST_BLOB_NUMBER: Record = { devnet: 0, }; -export function getFirstBlobNumber(): number { - return NETWORKS_FIRST_BLOB_NUMBER[env.NEXT_PUBLIC_NETWORK_NAME]; +export function getFirstBlobNumber(networkName: string): number | undefined { + return NETWORKS_FIRST_BLOB_NUMBER[networkName]; } export type NavigationItem = { @@ -61,45 +58,48 @@ export function isExpandibleNavigationItem( return typeof item === "object" && item !== null && "items" in item; } -export const NAVIGATION_ITEMS: Array< - NavigationItem | ExpandibleNavigationItem -> = [ - { - label: "Blockchain", - icon: , - items: [ - { - label: "Blobs", - href: buildBlobsRoute(), - }, - { - label: "Blocks", - href: buildBlocksRoute(), - }, - { - label: "Transactions", - href: buildTransactionsRoute(), - }, - ], - }, - { - label: "Networks", - icon: , - items: JSON.parse(env.NEXT_PUBLIC_SUPPORTED_NETWORKS || "[]"), - }, - { - label: "Stats", - icon: , - href: buildAllStatsRoute(), - }, - { - label: "API", - icon: , - href: resolveApiUrl(), - }, - { - label: "Docs", - icon: , - href: "https://docs.blobscan.com", - }, -]; +export const getNavigationItems = ( + networkName: string, + publicSupportedNetworks: string +): Array => { + return [ + { + label: "Blockchain", + icon: , + items: [ + { + label: "Blobs", + href: buildBlobsRoute(), + }, + { + label: "Blocks", + href: buildBlocksRoute(), + }, + { + label: "Transactions", + href: buildTransactionsRoute(), + }, + ], + }, + { + label: "Networks", + icon: , + items: JSON.parse(publicSupportedNetworks || "[]"), + }, + { + label: "Stats", + icon: , + href: buildAllStatsRoute(), + }, + { + label: "API", + icon: , + href: resolveApiUrl(networkName), + }, + { + label: "Docs", + icon: , + href: "https://docs.blobscan.com", + }, + ]; +}; diff --git a/apps/web/src/env.mjs b/apps/web/src/env.mjs index 0e6d34f9a..4aefb15a6 100644 --- a/apps/web/src/env.mjs +++ b/apps/web/src/env.mjs @@ -29,32 +29,6 @@ export const env = createEnv({ METRICS_ENABLED: booleanSchema.default("false"), TRACES_ENABLED: booleanSchema.default("false"), }, - /** - * Specify your client-side environment variables schema here. - * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. - */ - client: { - NEXT_PUBLIC_BEACON_BASE_URL: z - .string() - .url() - .default("https://beaconcha.in/"), - NEXT_PUBLIC_BLOBSCAN_RELEASE: z.string().optional(), - NEXT_PUBLIC_EXPLORER_BASE_URL: z - .string() - .url() - .default("https://etherscan.io/"), - NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), - NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), - NEXT_PUBLIC_POSTHOG_ID: z.string().optional(), - NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), - NEXT_PUBLIC_SUPPORTED_NETWORKS: z - .string() - .default( - '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]' - ), - NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: booleanSchema.default("false"), - NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: z.string().optional(), - }, /** * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. */ @@ -64,19 +38,6 @@ export const env = createEnv({ METRICS_ENABLED: process.env.METRICS_ENABLED, NODE_ENV: process.env.NODE_ENV, TRACES_ENABLED: process.env.TRACES_ENABLED, - - NEXT_PUBLIC_BLOBSCAN_RELEASE: process.env.NEXT_PUBLIC_BLOBSCAN_RELEASE, - NEXT_PUBLIC_BEACON_BASE_URL: process.env.NEXT_PUBLIC_BEACON_BASE_URL, - NEXT_PUBLIC_EXPLORER_BASE_URL: process.env.NEXT_PUBLIC_EXPLORER_BASE_URL, - NEXT_PUBLIC_NETWORK_NAME: process.env.NEXT_PUBLIC_NETWORK_NAME, - NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, - NEXT_PUBLIC_POSTHOG_ID: process.env.NEXT_PUBLIC_POSTHOG_ID, - NEXT_PUBLIC_SENTRY_DSN_WEB: process.env.NEXT_PUBLIC_SENTRY_DSN_WEB, - NEXT_PUBLIC_SUPPORTED_NETWORKS: process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS, - NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: - process.env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED, - NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: - process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, }, skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 17347bd03..f6fb69f16 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -19,26 +19,33 @@ import { SkeletonTheme } from "react-loading-skeleton"; import AppLayout from "~/components/AppLayout/AppLayout"; import { FeedbackWidget } from "~/components/FeedbackWidget/FeedbackWidget"; import { api } from "~/api-client"; -import { env } from "~/env.mjs"; import { useIsMounted } from "~/hooks/useIsMounted"; import { BlobDecoderWorkerProvider } from "~/providers/BlobDecoderWorker"; - -if (typeof window !== "undefined" && env.NEXT_PUBLIC_POSTHOG_ID) { - posthog.init(env.NEXT_PUBLIC_POSTHOG_ID, { - api_host: env.NEXT_PUBLIC_POSTHOG_HOST, - person_profiles: "identified_only", - loaded: (posthog) => { - if (window.location.hostname.includes("localhost")) { - posthog.debug(); - } - }, - }); -} +import { EnvProvider, useEnv } from "~/providers/Env"; function App({ Component, pageProps }: NextAppProps) { const { resolvedTheme } = useTheme(); const isMounted = useIsMounted(); const router = useRouter(); + const { env, isLoading: envLoading } = useEnv(); + + useEffect(() => { + if ( + typeof window !== "undefined" && + !envLoading && + env["PUBLIC_POSTHOG_ID"] !== undefined + ) { + posthog.init(env["PUBLIC_POSTHOG_ID"] as string, { + api_host: env["PUBLIC_POSTHOG_HOST"] as string, + person_profiles: "identified_only", + loaded: (posthog) => { + if (window.location.hostname.includes("localhost")) { + posthog.debug(); + } + }, + }); + } + }, [env, envLoading]); useEffect(() => { const handleRouteChange = () => { @@ -85,7 +92,7 @@ function App({ Component, pageProps }: NextAppProps) { - {env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED && } + {!envLoading && env["PUBLIC_VERCEL_ANALYTICS_ENABLED"] && } ); @@ -95,7 +102,9 @@ function AppWrapper(props: NextAppProps) { return ( - + + + ); diff --git a/apps/web/src/pages/block/[id].tsx b/apps/web/src/pages/block/[id].tsx index c34101fdd..989d1eacf 100644 --- a/apps/web/src/pages/block/[id].tsx +++ b/apps/web/src/pages/block/[id].tsx @@ -16,6 +16,7 @@ import { BlockStatus } from "~/components/Status"; import { getFirstBlobNumber } from "~/components/content"; import { api } from "~/api-client"; import NextError from "~/pages/_error"; +import { useEnv } from "~/providers/Env"; import type { BlockWithExpandedBlobsAndTransactions } from "~/types"; import { BLOB_GAS_LIMIT_PER_BLOCK, @@ -55,6 +56,138 @@ const Block: NextPage = function () { const { data: latestBlock } = api.block.getLatestBlock.useQuery(); const blockNumber = blockData ? blockData.number : undefined; + const { env, isLoading: envLoading } = useEnv(); + const networkName = env["PUBLIC_NETWORK_NAME"] as string; + + const detailsFields: DetailsLayoutProps["fields"] | undefined = + useMemo(() => { + if (blockData) { + const totalBlockBlobSize = blockData?.transactions.reduce( + (acc, { blobs }) => { + const totalBlobsSize = blobs.reduce( + (blobAcc, { size }) => blobAcc + size, + 0 + ); + + return acc + totalBlobsSize; + }, + 0 + ); + const firstBlobNumber = !envLoading + ? getFirstBlobNumber(networkName) + : undefined; + + const previousBlockHref = + firstBlobNumber && blockNumber && firstBlobNumber < blockNumber + ? `/block_neighbor?blockNumber=${blockNumber}&direction=prev` + : undefined; + + return [ + { + name: "Block Height", + value: ( +
+ {blockData.number} + {blockNumber && previousBlockHref && ( + + )} +
+ ), + }, + { + name: "Status", + value: , + }, + { + name: "Hash", + value: , + }, + { + name: "Timestamp", + value: ( +
+ {formatTimestamp(blockData.timestamp)} +
+ ), + }, + { + name: "Slot", + value: ( + + {blockData.slot} + + ), + }, + { + name: "Blob Size", + value: ( +
+ {formatBytes(totalBlockBlobSize)} + + ({formatNumber(totalBlockBlobSize / GAS_PER_BLOB)}{" "} + {pluralize("blob", totalBlockBlobSize / GAS_PER_BLOB)}) + +
+ ), + }, + { + name: "Blob Gas Price", + value: , + }, + { + name: "Blob Gas Used", + value: , + }, + { + name: "Blob Gas Limit", + value: ( +
+ {formatNumber(BLOB_GAS_LIMIT_PER_BLOCK)} + + ({formatNumber(MAX_BLOBS_PER_BLOCK)}{" "} + {pluralize("blob", MAX_BLOBS_PER_BLOCK)} per block) + +
+ ), + }, + { + name: "Blob As Calldata Gas", + value: ( +
+ {formatNumber(blockData.blobAsCalldataGasUsed)} + + ( + + {formatNumber( + performDiv( + blockData.blobAsCalldataGasUsed, + blockData.blobGasUsed + ), + "standard", + { maximumFractionDigits: 2 } + )} + {" "} + times more expensive) + +
+ ), + }, + ]; + } + }, [blockData, networkName, latestBlock, blockNumber, envLoading]); + if (error) { return ( Block not found
; } - let detailsFields: DetailsLayoutProps["fields"] | undefined; - - if (blockData) { - const totalBlockBlobSize = blockData?.transactions.reduce( - (acc, { blobs }) => { - const totalBlobsSize = blobs.reduce( - (blobAcc, { size }) => blobAcc + size, - 0 - ); - - return acc + totalBlobsSize; - }, - 0 - ); - - detailsFields = [ - { - name: "Block Height", - value: ( -
- {blockData.number} - {blockNumber !== undefined && ( - - )} -
- ), - }, - { name: "Status", value: }, - { - name: "Hash", - value: , - }, - { - name: "Timestamp", - value: ( -
- {formatTimestamp(blockData.timestamp)} -
- ), - }, - { - name: "Slot", - value: ( - - {blockData.slot} - - ), - }, - { - name: "Blob Size", - value: ( -
- {formatBytes(totalBlockBlobSize)} - - ({formatNumber(totalBlockBlobSize / GAS_PER_BLOB)}{" "} - {pluralize("blob", totalBlockBlobSize / GAS_PER_BLOB)}) - -
- ), - }, - { - name: "Blob Gas Price", - value: , - }, - { - name: "Blob Gas Used", - value: , - }, - { - name: "Blob Gas Limit", - value: ( -
- {formatNumber(BLOB_GAS_LIMIT_PER_BLOCK)} - - ({formatNumber(MAX_BLOBS_PER_BLOCK)}{" "} - {pluralize("blob", MAX_BLOBS_PER_BLOCK)} per block) - -
- ), - }, - { - name: "Blob As Calldata Gas", - value: ( -
- {formatNumber(blockData.blobAsCalldataGasUsed)} - - ( - - {formatNumber( - performDiv( - blockData.blobAsCalldataGasUsed, - blockData.blobGasUsed - ), - "standard", - { maximumFractionDigits: 2 } - )} - {" "} - times more expensive) - -
- ), - }, - ]; - } - return ( <> ; + isLoading: boolean; +} + +//TODO: discuss if we should have a default value +const DEFAULT_ENV = { + PUBLIC_BEACON_BASE_URL: "https://beaconcha.in/", + PUBLIC_BLOBSCAN_RELEASE: undefined, + PUBLIC_EXPLORER_BASE_URL: "https://etherscan.io/", + PUBLIC_NETWORK_NAME: "mainnet", + PUBLIC_SENTRY_DSN_WEB: undefined, + PUBLIC_POSTHOG_ID: undefined, + PUBLIC_POSTHOG_HOST: "https://us.i.posthog.com", + PUBLIC_SUPPORTED_NETWORKS: + '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]', + PUBLIC_VERCEL_ANALYTICS_ENABLED: false, + PUBLIC_VERCEL_GIT_COMMIT_SHA: undefined, +}; + +const LOCAL_STORAGE_KEY = "env"; + +const EnvContext = createContext({ + env: DEFAULT_ENV, + isLoading: true, +}); + +export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { data: fetchedEnv, isLoading } = api.getEnv.useQuery(); + const [env, setEnv] = useState>( + () => { + let storedEnv; + if (global?.window !== undefined) localStorage.getItem(LOCAL_STORAGE_KEY); + return storedEnv ? JSON.parse(storedEnv) : DEFAULT_ENV; + } + ); + + useEffect(() => { + if ( + typeof window !== undefined && + localStorage && + fetchedEnv && + !isLoading + ) { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fetchedEnv)); + setEnv(fetchedEnv); + } + }, [fetchedEnv, isLoading]); + + return ( + + {children} + + ); +}; + +export const useEnv = () => { + const context = useContext(EnvContext); + if (!context) { + throw new Error("useEnv must be used within an EnvProvider"); + } + return context; +}; diff --git a/apps/web/src/utils/env.ts b/apps/web/src/utils/env.ts new file mode 100644 index 000000000..df443d909 --- /dev/null +++ b/apps/web/src/utils/env.ts @@ -0,0 +1,10 @@ +export const fetchEnv = async () => { + // TODO: adapt for any environmet + const response = await fetch("http://localhost:3000/api/trpc/getEnv"); + + if (!response.ok) { + throw new Error("Failed to fetch envs for sentry server init config."); + } + + return await response.json(); +}; diff --git a/packages/api/src/app-router.ts b/packages/api/src/app-router.ts index 5bf5b7e66..695bcb4e7 100644 --- a/packages/api/src/app-router.ts +++ b/packages/api/src/app-router.ts @@ -1,5 +1,6 @@ import { z } from "@blobscan/zod"; +import { getEnv } from "./getEnv"; import { publicProcedure } from "./procedures"; import { blobRouter } from "./routers/blob"; import { blobStoragesStateRouter } from "./routers/blob-storages-state"; @@ -32,6 +33,7 @@ export const appRouter = t.router({ .input(z.void()) .output(z.string()) .query(() => "yay!"), + getEnv: getEnv, }); // export type definition of API diff --git a/packages/api/src/getEnv.ts b/packages/api/src/getEnv.ts new file mode 100644 index 000000000..87c1f0fda --- /dev/null +++ b/packages/api/src/getEnv.ts @@ -0,0 +1,11 @@ +import { env } from "@blobscan/env"; + +import { publicProcedure } from "./procedures"; + +export const getEnv = publicProcedure.query(() => { + const clientEnv = Object.entries(env) + .filter(([key]) => key.startsWith("NEXT_")) + .map(([key, value]) => [key.replace(/^NEXT_/, ""), value]); + + return Object.fromEntries(clientEnv); +}); diff --git a/packages/env/index.ts b/packages/env/index.ts index d751e3753..98ae3409b 100644 --- a/packages/env/index.ts +++ b/packages/env/index.ts @@ -87,6 +87,29 @@ export const env = createEnv({ TEST: booleanSchema.optional(), TRACES_ENABLED: booleanSchema.default("false"), }, + clientPrefix: "NEXT", + client: { + NEXT_PUBLIC_BEACON_BASE_URL: z + .string() + .url() + .default("https://beaconcha.in/"), + NEXT_PUBLIC_BLOBSCAN_RELEASE: z.string().optional(), + NEXT_PUBLIC_EXPLORER_BASE_URL: z + .string() + .url() + .default("https://etherscan.io/"), + NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), + NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), + NEXT_PUBLIC_POSTHOG_ID: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), + NEXT_PUBLIC_SUPPORTED_NETWORKS: z + .string() + .default( + '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]' + ), + NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: booleanSchema.default("false"), + NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: z.string().optional(), + }, ...presetEnvOptions, }, From fe5837ebabf643b95a7bce7a2c9f74af52e19c68 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Thu, 9 Jan 2025 15:58:51 +0700 Subject: [PATCH 02/12] feat(web): adapt server side sentry env config for any environment --- apps/web/src/api-client.ts | 2 +- apps/web/src/utils/env.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/api-client.ts b/apps/web/src/api-client.ts index 92f80cac4..a2e64c9a6 100644 --- a/apps/web/src/api-client.ts +++ b/apps/web/src/api-client.ts @@ -4,7 +4,7 @@ import superjson from "superjson"; import type { AppRouter } from "@blobscan/api"; -const getBaseUrl = () => { +export const getBaseUrl = () => { if (typeof window !== "undefined") return ""; // browser should use relative url if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url diff --git a/apps/web/src/utils/env.ts b/apps/web/src/utils/env.ts index df443d909..fecd9e551 100644 --- a/apps/web/src/utils/env.ts +++ b/apps/web/src/utils/env.ts @@ -1,6 +1,7 @@ +import { getBaseUrl } from "~/api-client"; + export const fetchEnv = async () => { - // TODO: adapt for any environmet - const response = await fetch("http://localhost:3000/api/trpc/getEnv"); + const response = await fetch(`${getBaseUrl()}/api/trpc/getEnv`); if (!response.ok) { throw new Error("Failed to fetch envs for sentry server init config."); From ce4bb56bc0c997f329cef35b16c320a583576e7d Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Thu, 9 Jan 2025 16:29:13 +0700 Subject: [PATCH 03/12] fix(web): limit to '1' env query --- apps/web/src/providers/Env.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/providers/Env.tsx b/apps/web/src/providers/Env.tsx index 5ea605d07..b17f68d59 100644 --- a/apps/web/src/providers/Env.tsx +++ b/apps/web/src/providers/Env.tsx @@ -32,7 +32,10 @@ const EnvContext = createContext({ export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const { data: fetchedEnv, isLoading } = api.getEnv.useQuery(); + const { data: fetchedEnv, isLoading } = api.getEnv.useQuery(undefined, { + staleTime: Infinity, + cacheTime: Infinity, + }); const [env, setEnv] = useState>( () => { let storedEnv; From c3d89caee191205e1d9a536c1d405b011678cdf6 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 13 Jan 2025 10:27:06 +0700 Subject: [PATCH 04/12] refactor(web): reset 'server' and 'edge' sentry config --- apps/web/sentry.edge.config.ts | 23 ++++++++--------------- apps/web/sentry.server.config.ts | 23 ++++++++--------------- apps/web/src/env.mjs | 7 +++++++ 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/apps/web/sentry.edge.config.ts b/apps/web/sentry.edge.config.ts index 634ac2556..c552e322b 100644 --- a/apps/web/sentry.edge.config.ts +++ b/apps/web/sentry.edge.config.ts @@ -5,18 +5,11 @@ import * as Sentry from "@sentry/nextjs"; -import { fetchEnv } from "~/utils/env"; - -const initSentry = async () => { - const res = await fetchEnv(); - const env = res?.result?.data?.json as Record; - - Sentry.init({ - dsn: env["NEXT_PUBLIC_SENTRY_DSN_WEB"], - environment: env["NEXT_PUBLIC_NETWORK_NAME"], - tracesSampleRate: 1, - debug: false, - }); -}; - -initSentry(); +import { env } from "./src/env.mjs"; + +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN_WEB, + environment: env.NEXT_PUBLIC_NETWORK_NAME, + tracesSampleRate: 1, + debug: false, +}); diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index 95653b93b..48eaca44b 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -1,21 +1,14 @@ // This file configures the initialization of Sentry on the server. // The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javasccript/guides/nextjs/ +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ import * as Sentry from "@sentry/nextjs"; -import { fetchEnv } from "~/utils/env"; +import { env } from "./src/env.mjs"; -const initSentry = async () => { - const res = await fetchEnv(); - const env = res?.result?.data?.json as Record; - - Sentry.init({ - dsn: env["NEXT_PUBLIC_SENTRY_DSN_WEB"], - environment: env["NEXT_PUBLIC_NETWORK_NAME"], - tracesSampleRate: 1, - debug: false, - }); -}; - -initSentry(); +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN_WEB, + environment: env.NEXT_PUBLIC_NETWORK_NAME, + tracesSampleRate: 1, + debug: false, +}); diff --git a/apps/web/src/env.mjs b/apps/web/src/env.mjs index 4aefb15a6..3347899ca 100644 --- a/apps/web/src/env.mjs +++ b/apps/web/src/env.mjs @@ -29,6 +29,10 @@ export const env = createEnv({ METRICS_ENABLED: booleanSchema.default("false"), TRACES_ENABLED: booleanSchema.default("false"), }, + client: { + NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), + NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), + }, /** * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. */ @@ -38,6 +42,9 @@ export const env = createEnv({ METRICS_ENABLED: process.env.METRICS_ENABLED, NODE_ENV: process.env.NODE_ENV, TRACES_ENABLED: process.env.TRACES_ENABLED, + + NEXT_PUBLIC_NETWORK_NAME: process.env.NEXT_PUBLIC_NETWORK_NAME, + NEXT_PUBLIC_SENTRY_DSN_WEB: process.env.NEXT_PUBLIC_SENTRY_DSN_WEB, }, skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, }); From 652b4acf7b8ea8fb7e22377102cef21cea5d4178 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 13 Jan 2025 10:53:22 +0700 Subject: [PATCH 05/12] refactor(web): move env endpoint from API package to Next API --- apps/web/src/pages/api/env/index.ts | 22 ++++++++++++++ apps/web/src/providers/Env.tsx | 46 +++++++++++++---------------- packages/api/src/app-router.ts | 2 -- packages/api/src/getEnv.ts | 11 ------- 4 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 apps/web/src/pages/api/env/index.ts delete mode 100644 packages/api/src/getEnv.ts diff --git a/apps/web/src/pages/api/env/index.ts b/apps/web/src/pages/api/env/index.ts new file mode 100644 index 000000000..5b93c66f5 --- /dev/null +++ b/apps/web/src/pages/api/env/index.ts @@ -0,0 +1,22 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { env } from "@blobscan/env"; + +export default async function (req: NextApiRequest, res: NextApiResponse) { + const method = req.method; + + switch (method) { + case "POST": { + const clientEnv = Object.entries(env) + .filter(([key]) => key.startsWith("NEXT_")) + .map(([key, value]) => [key.replace(/^NEXT_/, ""), value]); + + return res.status(200).json({ + data: Object.fromEntries(clientEnv), + }); + } + + default: + throw new Error("Method not allowed"); + } +} diff --git a/apps/web/src/providers/Env.tsx b/apps/web/src/providers/Env.tsx index b17f68d59..d4eac7afb 100644 --- a/apps/web/src/providers/Env.tsx +++ b/apps/web/src/providers/Env.tsx @@ -1,7 +1,5 @@ import React, { createContext, useContext, useState, useEffect } from "react"; -import { api } from "~/api-client"; - interface EnvContextType { env: Record; isLoading: boolean; @@ -22,8 +20,6 @@ const DEFAULT_ENV = { PUBLIC_VERCEL_GIT_COMMIT_SHA: undefined, }; -const LOCAL_STORAGE_KEY = "env"; - const EnvContext = createContext({ env: DEFAULT_ENV, isLoading: true, @@ -32,32 +28,30 @@ const EnvContext = createContext({ export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const { data: fetchedEnv, isLoading } = api.getEnv.useQuery(undefined, { - staleTime: Infinity, - cacheTime: Infinity, - }); - const [env, setEnv] = useState>( - () => { - let storedEnv; - if (global?.window !== undefined) localStorage.getItem(LOCAL_STORAGE_KEY); - return storedEnv ? JSON.parse(storedEnv) : DEFAULT_ENV; - } - ); + const [env, setEnv] = + useState>(); useEffect(() => { - if ( - typeof window !== undefined && - localStorage && - fetchedEnv && - !isLoading - ) { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fetchedEnv)); - setEnv(fetchedEnv); - } - }, [fetchedEnv, isLoading]); + const fetchEnv = async () => { + try { + const request = await fetch("/api/env", { method: "POST" }); + const data = await request.json(); + setEnv(data.data); + } catch (error) { + console.error( + "Error fetching environment variables from server side:", + error + ); + } + }; + + fetchEnv(); + }, []); return ( - + {children} ); diff --git a/packages/api/src/app-router.ts b/packages/api/src/app-router.ts index 695bcb4e7..5bf5b7e66 100644 --- a/packages/api/src/app-router.ts +++ b/packages/api/src/app-router.ts @@ -1,6 +1,5 @@ import { z } from "@blobscan/zod"; -import { getEnv } from "./getEnv"; import { publicProcedure } from "./procedures"; import { blobRouter } from "./routers/blob"; import { blobStoragesStateRouter } from "./routers/blob-storages-state"; @@ -33,7 +32,6 @@ export const appRouter = t.router({ .input(z.void()) .output(z.string()) .query(() => "yay!"), - getEnv: getEnv, }); // export type definition of API diff --git a/packages/api/src/getEnv.ts b/packages/api/src/getEnv.ts deleted file mode 100644 index 87c1f0fda..000000000 --- a/packages/api/src/getEnv.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { env } from "@blobscan/env"; - -import { publicProcedure } from "./procedures"; - -export const getEnv = publicProcedure.query(() => { - const clientEnv = Object.entries(env) - .filter(([key]) => key.startsWith("NEXT_")) - .map(([key, value]) => [key.replace(/^NEXT_/, ""), value]); - - return Object.fromEntries(clientEnv); -}); From d5b8a88f543f59571fd25b39bef325c649120c56 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 13 Jan 2025 11:13:32 +0700 Subject: [PATCH 06/12] feat: remove default env value --- .../src/components/BlobscanVersionInfo.tsx | 4 +-- apps/web/src/components/ExplorerDetails.tsx | 8 ++--- apps/web/src/components/Filters/index.tsx | 6 ++-- apps/web/src/components/NavigationMenus.tsx | 15 ++++++---- .../src/components/SidebarNavigationMenu.tsx | 14 +++++---- apps/web/src/pages/_app.tsx | 8 ++--- apps/web/src/pages/block/[id].tsx | 8 ++--- apps/web/src/providers/Env.tsx | 29 ++----------------- 8 files changed, 39 insertions(+), 53 deletions(-) diff --git a/apps/web/src/components/BlobscanVersionInfo.tsx b/apps/web/src/components/BlobscanVersionInfo.tsx index 4c09d8930..b4df56e37 100644 --- a/apps/web/src/components/BlobscanVersionInfo.tsx +++ b/apps/web/src/components/BlobscanVersionInfo.tsx @@ -7,12 +7,12 @@ export const BlobscanVersionInfo: React.FC = () => { let url = "https://github.com/Blobscan/blobscan/"; let label = "Development"; - if (env["PUBLIC_BLOBSCAN_RELEASE"]) { + if (env && env["PUBLIC_BLOBSCAN_RELEASE"]) { url = `https://github.com/Blobscan/blobscan/releases/tag/${env["PUBLIC_BLOBSCAN_RELEASE"]}`; label = env["PUBLIC_BLOBSCAN_RELEASE"] as string; } - if (env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]) { + if (env && env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]) { url = `https://github.com/Blobscan/blobscan/commit/${env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]}`; label = (env["PUBLIC_VERCEL_GIT_COMMIT_SHA"] as string).slice(0, 7); } diff --git a/apps/web/src/components/ExplorerDetails.tsx b/apps/web/src/components/ExplorerDetails.tsx index 0ab45195f..0ba0d4f09 100644 --- a/apps/web/src/components/ExplorerDetails.tsx +++ b/apps/web/src/components/ExplorerDetails.tsx @@ -45,7 +45,7 @@ export function ExplorerDetails({ placement }: ExplorerDetailsProps) { const { data: blobStoragesState } = api.blobStoragesState.getState.useQuery(); const { data: latestBlock } = api.block.getLatestBlock.useQuery(); - const { env, isLoading: envLoading } = useEnv(); + const { env } = useEnv(); const explorerDetailsItems: ExplorerDetailsItemProps[] = []; @@ -53,10 +53,10 @@ export function ExplorerDetails({ placement }: ExplorerDetailsProps) { explorerDetailsItems.push( { name: "Network", - value: envLoading ? ( - - ) : ( + value: env ? ( capitalize(env["PUBLIC_NETWORK_NAME"] as string) + ) : ( + ), }, { diff --git a/apps/web/src/components/Filters/index.tsx b/apps/web/src/components/Filters/index.tsx index ffd7e432c..9f00892c5 100644 --- a/apps/web/src/components/Filters/index.tsx +++ b/apps/web/src/components/Filters/index.tsx @@ -91,7 +91,7 @@ export const Filters: FC = function () { const router = useRouter(); const queryParams = useQueryParams(); const [filters, dispatch] = useReducer(reducer, INIT_STATE); - const { env, isLoading: envLoading } = useEnv(); + const { env } = useEnv(); const disableClear = !filters.category && @@ -171,7 +171,7 @@ export const Filters: FC = function () { const rollupOptions: DropdownProps["options"] = useMemo(() => { const chainId = - !envLoading && getChainIdByName(env["PUBLIC_NETWORK_NAME"] as string); + env && getChainIdByName(env["PUBLIC_NETWORK_NAME"] as string); const rollups = chainId ? getChainRollups(chainId) : []; return rollups.map( @@ -189,7 +189,7 @@ export const Filters: FC = function () { ), } satisfies Option) ); - }, [env, envLoading]); + }, [env]); useEffect(() => { const { sort } = queryParams.paginationParams; diff --git a/apps/web/src/components/NavigationMenus.tsx b/apps/web/src/components/NavigationMenus.tsx index f8c92749c..e214116cd 100644 --- a/apps/web/src/components/NavigationMenus.tsx +++ b/apps/web/src/components/NavigationMenus.tsx @@ -16,15 +16,20 @@ import type { ExpandibleNavigationItem, NavigationItem } from "./content"; import { getNavigationItems, isExpandibleNavigationItem } from "./content"; export const NavigationMenus: FC = () => { - const { env, isLoading: envLoading } = useEnv(); + const { env } = useEnv(); const navigationItems = useMemo(() => { - const networkName = env["PUBLIC_NETWORK_NAME"] as string; - const publicSupportedNetworks = env["PUBLIC_SUPPORTED_NETWORKS"] as string; - return !envLoading + const networkName = env + ? (env["PUBLIC_NETWORK_NAME"] as string) + : undefined; + const publicSupportedNetworks = env + ? (env["PUBLIC_SUPPORTED_NETWORKS"] as string) + : undefined; + + return networkName && publicSupportedNetworks ? getNavigationItems(networkName, publicSupportedNetworks) : undefined; - }, [envLoading, env]); + }, [env]); if (!navigationItems) { return ; diff --git a/apps/web/src/components/SidebarNavigationMenu.tsx b/apps/web/src/components/SidebarNavigationMenu.tsx index 8e699f6f3..3c1c86820 100644 --- a/apps/web/src/components/SidebarNavigationMenu.tsx +++ b/apps/web/src/components/SidebarNavigationMenu.tsx @@ -23,15 +23,19 @@ export function SidebarNavigationMenu({ className }: { className?: string }) { const closeSidebar = useCallback(() => setOpen(false), []); - const { env, isLoading: envLoading } = useEnv(); + const { env } = useEnv(); const navigationItems = useMemo(() => { - const networkName = env["PUBLIC_NETWORK_NAME"] as string; - const publicSupportedNetworks = env["PUBLIC_SUPPORTED_NETWORKS"] as string; - return !envLoading + const networkName = env + ? (env["PUBLIC_NETWORK_NAME"] as string) + : undefined; + const publicSupportedNetworks = env + ? (env["PUBLIC_SUPPORTED_NETWORKS"] as string) + : undefined; + return networkName && publicSupportedNetworks ? getNavigationItems(networkName, publicSupportedNetworks) : undefined; - }, [envLoading, env]); + }, [env]); return (
diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index f6fb69f16..93991b3b2 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -27,12 +27,12 @@ function App({ Component, pageProps }: NextAppProps) { const { resolvedTheme } = useTheme(); const isMounted = useIsMounted(); const router = useRouter(); - const { env, isLoading: envLoading } = useEnv(); + const { env } = useEnv(); useEffect(() => { if ( typeof window !== "undefined" && - !envLoading && + env && env["PUBLIC_POSTHOG_ID"] !== undefined ) { posthog.init(env["PUBLIC_POSTHOG_ID"] as string, { @@ -45,7 +45,7 @@ function App({ Component, pageProps }: NextAppProps) { }, }); } - }, [env, envLoading]); + }, [env]); useEffect(() => { const handleRouteChange = () => { @@ -92,7 +92,7 @@ function App({ Component, pageProps }: NextAppProps) { - {!envLoading && env["PUBLIC_VERCEL_ANALYTICS_ENABLED"] && } + {env && env["PUBLIC_VERCEL_ANALYTICS_ENABLED"] && } ); diff --git a/apps/web/src/pages/block/[id].tsx b/apps/web/src/pages/block/[id].tsx index 989d1eacf..bd897dc0a 100644 --- a/apps/web/src/pages/block/[id].tsx +++ b/apps/web/src/pages/block/[id].tsx @@ -56,8 +56,8 @@ const Block: NextPage = function () { const { data: latestBlock } = api.block.getLatestBlock.useQuery(); const blockNumber = blockData ? blockData.number : undefined; - const { env, isLoading: envLoading } = useEnv(); - const networkName = env["PUBLIC_NETWORK_NAME"] as string; + const { env } = useEnv(); + const networkName = env ? (env["PUBLIC_NETWORK_NAME"] as string) : undefined; const detailsFields: DetailsLayoutProps["fields"] | undefined = useMemo(() => { @@ -73,7 +73,7 @@ const Block: NextPage = function () { }, 0 ); - const firstBlobNumber = !envLoading + const firstBlobNumber = networkName ? getFirstBlobNumber(networkName) : undefined; @@ -186,7 +186,7 @@ const Block: NextPage = function () { }, ]; } - }, [blockData, networkName, latestBlock, blockNumber, envLoading]); + }, [blockData, networkName, latestBlock, blockNumber]); if (error) { return ( diff --git a/apps/web/src/providers/Env.tsx b/apps/web/src/providers/Env.tsx index d4eac7afb..ffd1800e8 100644 --- a/apps/web/src/providers/Env.tsx +++ b/apps/web/src/providers/Env.tsx @@ -1,29 +1,10 @@ import React, { createContext, useContext, useState, useEffect } from "react"; interface EnvContextType { - env: Record; - isLoading: boolean; + env?: Record; } -//TODO: discuss if we should have a default value -const DEFAULT_ENV = { - PUBLIC_BEACON_BASE_URL: "https://beaconcha.in/", - PUBLIC_BLOBSCAN_RELEASE: undefined, - PUBLIC_EXPLORER_BASE_URL: "https://etherscan.io/", - PUBLIC_NETWORK_NAME: "mainnet", - PUBLIC_SENTRY_DSN_WEB: undefined, - PUBLIC_POSTHOG_ID: undefined, - PUBLIC_POSTHOG_HOST: "https://us.i.posthog.com", - PUBLIC_SUPPORTED_NETWORKS: - '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]', - PUBLIC_VERCEL_ANALYTICS_ENABLED: false, - PUBLIC_VERCEL_GIT_COMMIT_SHA: undefined, -}; - -const EnvContext = createContext({ - env: DEFAULT_ENV, - isLoading: true, -}); +const EnvContext = createContext({ env: undefined }); export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ children, @@ -49,11 +30,7 @@ export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); return ( - - {children} - + {children} ); }; From bbf2fbca0c188411afa9d80af4e33804f97e8bb2 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 13 Jan 2025 11:33:13 +0700 Subject: [PATCH 07/12] feat(web): use Next env api endpoint to init sentry in clientside --- apps/web/sentry.client.config.ts | 61 ++++++++++---------------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index 260ae45c1..0984a358f 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -4,49 +4,26 @@ import * as Sentry from "@sentry/nextjs"; -// DISCUSS: on the first load of the app, there is no value, should we put a fallback? or retry the operation til there is a value in -// the local storage? After a first load, always works -const getEnvFromLocalStorage = (key: string) => { - if (typeof window !== "undefined") { - try { - const storedEnv = localStorage.getItem("env"); - if (storedEnv) { - const parsedEnv = JSON.parse(storedEnv) as Record; - return parsedEnv[`${key}`]; - } - } catch (error) { - console.error("Failed to read env from localStorage:", error); - } +const initSentry = async () => { + try { + const request = await fetch("/api/env", { method: "POST" }); + const env = (await request.json()).data; + + const dns = env["PUBLIC_SENTRY_DSN_WEB"]; + const environment = env["PUBLIC_NETWORK_NAME"]; + + Sentry.init({ + dsn: dns, + environment, + tracesSampleRate: 1, + debug: false, + }); + } catch (error) { + console.error( + "Error fetching environment variables from server side to init sentry client:", + error + ); } - return undefined; -}; - -const initSentry = () => { - const retryInterval = 1000; - const maxRetries = 10; - let retries = 0; - - const interval = setInterval(() => { - const dns = getEnvFromLocalStorage("PUBLIC_SENTRY_DSN_WEB"); - const environment = getEnvFromLocalStorage("PUBLIC_NETWORK_NAME"); - - if (environment || dns) { - Sentry.init({ - dsn: dns, - environment, - tracesSampleRate: 1, - debug: false, - }); - - clearInterval(interval); - } - - retries += 1; - if (retries >= maxRetries) { - clearInterval(interval); - console.warn("Failed to initialize Sentry after maximum retries."); - } - }, retryInterval); }; initSentry(); From b0ef04c35825e4002656299d5fac7c946018b022 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 13 Jan 2025 12:21:27 +0700 Subject: [PATCH 08/12] chore(web): remove unnecessary export --- apps/web/src/api-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/api-client.ts b/apps/web/src/api-client.ts index a2e64c9a6..92f80cac4 100644 --- a/apps/web/src/api-client.ts +++ b/apps/web/src/api-client.ts @@ -4,7 +4,7 @@ import superjson from "superjson"; import type { AppRouter } from "@blobscan/api"; -export const getBaseUrl = () => { +const getBaseUrl = () => { if (typeof window !== "undefined") return ""; // browser should use relative url if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url From b88ada6f4524bde138b38836a4e105942f8394c8 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 13 Jan 2025 12:25:31 +0700 Subject: [PATCH 09/12] refactor(web): remove unnecessary env fetch utils file --- apps/web/src/utils/env.ts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 apps/web/src/utils/env.ts diff --git a/apps/web/src/utils/env.ts b/apps/web/src/utils/env.ts deleted file mode 100644 index fecd9e551..000000000 --- a/apps/web/src/utils/env.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getBaseUrl } from "~/api-client"; - -export const fetchEnv = async () => { - const response = await fetch(`${getBaseUrl()}/api/trpc/getEnv`); - - if (!response.ok) { - throw new Error("Failed to fetch envs for sentry server init config."); - } - - return await response.json(); -}; From c12a61d5d7188f3ac1e230fb5d195fb4d1f6efed Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 27 Jan 2025 10:05:54 +0700 Subject: [PATCH 10/12] feat(web): validate env API request json parsing --- apps/web/sentry.client.config.ts | 9 ++---- apps/web/src/env.mjs | 49 ++++++++++++++++++++++------- apps/web/src/pages/api/env/index.ts | 29 +++++++++++------ apps/web/src/providers/Env.tsx | 4 +-- packages/env/index.ts | 23 -------------- 5 files changed, 63 insertions(+), 51 deletions(-) diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index 0984a358f..74a300120 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -6,8 +6,8 @@ import * as Sentry from "@sentry/nextjs"; const initSentry = async () => { try { - const request = await fetch("/api/env", { method: "POST" }); - const env = (await request.json()).data; + const request = await fetch("/api/env"); + const env = await request.json(); const dns = env["PUBLIC_SENTRY_DSN_WEB"]; const environment = env["PUBLIC_NETWORK_NAME"]; @@ -19,10 +19,7 @@ const initSentry = async () => { debug: false, }); } catch (error) { - console.error( - "Error fetching environment variables from server side to init sentry client:", - error - ); + console.error("Error during Sentry initialization", error); } }; diff --git a/apps/web/src/env.mjs b/apps/web/src/env.mjs index 3347899ca..61f10ee28 100644 --- a/apps/web/src/env.mjs +++ b/apps/web/src/env.mjs @@ -1,13 +1,6 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; -// See booleanSchema from packages/zod/src/schemas.ts -// We need to redefine it because we can't import ts files from here -const booleanSchema = z - .string() - .refine((s) => s === "true" || s === "false") - .transform((s) => s === "true"); - const networkSchema = z.enum([ "mainnet", "holesky", @@ -17,6 +10,31 @@ const networkSchema = z.enum([ "devnet", ]); +const clientEnvVars = { + NEXT_PUBLIC_BEACON_BASE_URL: z + .string() + .url() + .default("https://beaconcha.in/"), + NEXT_PUBLIC_BLOBSCAN_RELEASE: z.string().optional(), + NEXT_PUBLIC_EXPLORER_BASE_URL: z + .string() + .url() + .default("https://etherscan.io/"), + NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), + NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), + NEXT_PUBLIC_POSTHOG_ID: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), + NEXT_PUBLIC_SUPPORTED_NETWORKS: z + .string() + .default( + '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]' + ), + NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: z.boolean().default(false), + NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: z.string().optional(), +}; + +export const clientEnvVarsSchema = z.object(clientEnvVars); + export const env = createEnv({ /** * Specify your server-side environment variables schema here. This way you can ensure the app isn't @@ -26,12 +44,11 @@ export const env = createEnv({ DATABASE_URL: z.string().url(), FEEDBACK_WEBHOOK_URL: z.string().optional(), NODE_ENV: z.enum(["development", "test", "production"]), - METRICS_ENABLED: booleanSchema.default("false"), - TRACES_ENABLED: booleanSchema.default("false"), + METRICS_ENABLED: z.boolean().default(false), + TRACES_ENABLED: z.boolean().default(false), }, client: { - NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), - NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), + ...clientEnvVars, }, /** * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. @@ -43,8 +60,18 @@ export const env = createEnv({ NODE_ENV: process.env.NODE_ENV, TRACES_ENABLED: process.env.TRACES_ENABLED, + NEXT_PUBLIC_BLOBSCAN_RELEASE: process.env.NEXT_PUBLIC_BLOBSCAN_RELEASE, + NEXT_PUBLIC_BEACON_BASE_URL: process.env.NEXT_PUBLIC_BEACON_BASE_URL, + NEXT_PUBLIC_EXPLORER_BASE_URL: process.env.NEXT_PUBLIC_EXPLORER_BASE_URL, NEXT_PUBLIC_NETWORK_NAME: process.env.NEXT_PUBLIC_NETWORK_NAME, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + NEXT_PUBLIC_POSTHOG_ID: process.env.NEXT_PUBLIC_POSTHOG_ID, NEXT_PUBLIC_SENTRY_DSN_WEB: process.env.NEXT_PUBLIC_SENTRY_DSN_WEB, + NEXT_PUBLIC_SUPPORTED_NETWORKS: process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS, + NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: + process.env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED, + NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: + process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, }, skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/apps/web/src/pages/api/env/index.ts b/apps/web/src/pages/api/env/index.ts index 5b93c66f5..1189625ce 100644 --- a/apps/web/src/pages/api/env/index.ts +++ b/apps/web/src/pages/api/env/index.ts @@ -2,21 +2,32 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { env } from "@blobscan/env"; +import { clientEnvVarsSchema } from "../../../env.mjs"; + export default async function (req: NextApiRequest, res: NextApiResponse) { const method = req.method; switch (method) { - case "POST": { - const clientEnv = Object.entries(env) - .filter(([key]) => key.startsWith("NEXT_")) - .map(([key, value]) => [key.replace(/^NEXT_/, ""), value]); - - return res.status(200).json({ - data: Object.fromEntries(clientEnv), - }); + case "GET": { + const parsedEnv = clientEnvVarsSchema.safeParse(env); + + if (!parsedEnv.success) { + return res.status(400).json({ + error: "Error parsing client side environment variables", + details: parsedEnv.error.format(), + }); + } + + const clientEnv = Object.fromEntries( + Object.entries(parsedEnv.data) + .filter(([key]) => key.startsWith("NEXT_")) + .map(([key, value]) => [key.replace(/^NEXT_/, ""), value]) + ); + + return res.status(200).json(clientEnv); } default: - throw new Error("Method not allowed"); + return res.status(405).json({ error: "Method not allowed" }); } } diff --git a/apps/web/src/providers/Env.tsx b/apps/web/src/providers/Env.tsx index ffd1800e8..7e8d17239 100644 --- a/apps/web/src/providers/Env.tsx +++ b/apps/web/src/providers/Env.tsx @@ -15,9 +15,9 @@ export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { const fetchEnv = async () => { try { - const request = await fetch("/api/env", { method: "POST" }); + const request = await fetch("/api/env"); const data = await request.json(); - setEnv(data.data); + setEnv(data); } catch (error) { console.error( "Error fetching environment variables from server side:", diff --git a/packages/env/index.ts b/packages/env/index.ts index 98ae3409b..d751e3753 100644 --- a/packages/env/index.ts +++ b/packages/env/index.ts @@ -87,29 +87,6 @@ export const env = createEnv({ TEST: booleanSchema.optional(), TRACES_ENABLED: booleanSchema.default("false"), }, - clientPrefix: "NEXT", - client: { - NEXT_PUBLIC_BEACON_BASE_URL: z - .string() - .url() - .default("https://beaconcha.in/"), - NEXT_PUBLIC_BLOBSCAN_RELEASE: z.string().optional(), - NEXT_PUBLIC_EXPLORER_BASE_URL: z - .string() - .url() - .default("https://etherscan.io/"), - NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), - NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), - NEXT_PUBLIC_POSTHOG_ID: z.string().optional(), - NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), - NEXT_PUBLIC_SUPPORTED_NETWORKS: z - .string() - .default( - '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]' - ), - NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: booleanSchema.default("false"), - NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: z.string().optional(), - }, ...presetEnvOptions, }, From 85c704ab51dc26d9985af59f91600180c84a7478 Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Mon, 27 Jan 2025 10:06:08 +0700 Subject: [PATCH 11/12] chore(web): update changeset --- .changeset/sweet-beans-speak.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sweet-beans-speak.md diff --git a/.changeset/sweet-beans-speak.md b/.changeset/sweet-beans-speak.md new file mode 100644 index 000000000..ca095446e --- /dev/null +++ b/.changeset/sweet-beans-speak.md @@ -0,0 +1,5 @@ +--- +"@blobscan/web": minor +--- + +Get environment variables from API endpoint. From a84d3d69881bc459dbd1a6994604983bde50b4bd Mon Sep 17 00:00:00 2001 From: Fran Jimenez Aguilera Date: Tue, 28 Jan 2025 11:48:26 +0700 Subject: [PATCH 12/12] refactor(web): infer clien env zod schema in Env provider --- apps/web/src/components/BlobscanVersionInfo.tsx | 12 ++++++------ apps/web/src/components/ExplorerDetails.tsx | 2 +- apps/web/src/components/Filters/index.tsx | 3 +-- apps/web/src/components/NavigationMenus.tsx | 6 ++---- .../web/src/components/SidebarNavigationMenu.tsx | 6 ++---- apps/web/src/pages/_app.tsx | 8 ++++---- apps/web/src/pages/block/[id].tsx | 2 +- apps/web/src/providers/Env.tsx | 16 +++++++++++++--- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/apps/web/src/components/BlobscanVersionInfo.tsx b/apps/web/src/components/BlobscanVersionInfo.tsx index b4df56e37..a212e1c5c 100644 --- a/apps/web/src/components/BlobscanVersionInfo.tsx +++ b/apps/web/src/components/BlobscanVersionInfo.tsx @@ -7,14 +7,14 @@ export const BlobscanVersionInfo: React.FC = () => { let url = "https://github.com/Blobscan/blobscan/"; let label = "Development"; - if (env && env["PUBLIC_BLOBSCAN_RELEASE"]) { - url = `https://github.com/Blobscan/blobscan/releases/tag/${env["PUBLIC_BLOBSCAN_RELEASE"]}`; - label = env["PUBLIC_BLOBSCAN_RELEASE"] as string; + if (env && env.PUBLIC_BLOBSCAN_RELEASE) { + url = `https://github.com/Blobscan/blobscan/releases/tag/${env.PUBLIC_BLOBSCAN_RELEASE}`; + label = env.PUBLIC_BLOBSCAN_RELEASE; } - if (env && env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]) { - url = `https://github.com/Blobscan/blobscan/commit/${env["PUBLIC_VERCEL_GIT_COMMIT_SHA"]}`; - label = (env["PUBLIC_VERCEL_GIT_COMMIT_SHA"] as string).slice(0, 7); + if (env && env.PUBLIC_VERCEL_GIT_COMMIT_SHA) { + url = `https://github.com/Blobscan/blobscan/commit/${env.PUBLIC_VERCEL_GIT_COMMIT_SHA}`; + label = env.PUBLIC_VERCEL_GIT_COMMIT_SHA.slice(0, 7); } return ( diff --git a/apps/web/src/components/ExplorerDetails.tsx b/apps/web/src/components/ExplorerDetails.tsx index 0ba0d4f09..0cb2dde12 100644 --- a/apps/web/src/components/ExplorerDetails.tsx +++ b/apps/web/src/components/ExplorerDetails.tsx @@ -54,7 +54,7 @@ export function ExplorerDetails({ placement }: ExplorerDetailsProps) { { name: "Network", value: env ? ( - capitalize(env["PUBLIC_NETWORK_NAME"] as string) + capitalize(env.PUBLIC_NETWORK_NAME) ) : ( ), diff --git a/apps/web/src/components/Filters/index.tsx b/apps/web/src/components/Filters/index.tsx index 9f00892c5..34e9f6381 100644 --- a/apps/web/src/components/Filters/index.tsx +++ b/apps/web/src/components/Filters/index.tsx @@ -170,8 +170,7 @@ export const Filters: FC = function () { }; const rollupOptions: DropdownProps["options"] = useMemo(() => { - const chainId = - env && getChainIdByName(env["PUBLIC_NETWORK_NAME"] as string); + const chainId = env && getChainIdByName(env.PUBLIC_NETWORK_NAME); const rollups = chainId ? getChainRollups(chainId) : []; return rollups.map( diff --git a/apps/web/src/components/NavigationMenus.tsx b/apps/web/src/components/NavigationMenus.tsx index e214116cd..665fcccbc 100644 --- a/apps/web/src/components/NavigationMenus.tsx +++ b/apps/web/src/components/NavigationMenus.tsx @@ -19,11 +19,9 @@ export const NavigationMenus: FC = () => { const { env } = useEnv(); const navigationItems = useMemo(() => { - const networkName = env - ? (env["PUBLIC_NETWORK_NAME"] as string) - : undefined; + const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; const publicSupportedNetworks = env - ? (env["PUBLIC_SUPPORTED_NETWORKS"] as string) + ? env.PUBLIC_SUPPORTED_NETWORKS : undefined; return networkName && publicSupportedNetworks diff --git a/apps/web/src/components/SidebarNavigationMenu.tsx b/apps/web/src/components/SidebarNavigationMenu.tsx index 3c1c86820..8b38574f1 100644 --- a/apps/web/src/components/SidebarNavigationMenu.tsx +++ b/apps/web/src/components/SidebarNavigationMenu.tsx @@ -26,11 +26,9 @@ export function SidebarNavigationMenu({ className }: { className?: string }) { const { env } = useEnv(); const navigationItems = useMemo(() => { - const networkName = env - ? (env["PUBLIC_NETWORK_NAME"] as string) - : undefined; + const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; const publicSupportedNetworks = env - ? (env["PUBLIC_SUPPORTED_NETWORKS"] as string) + ? env.PUBLIC_SUPPORTED_NETWORKS : undefined; return networkName && publicSupportedNetworks ? getNavigationItems(networkName, publicSupportedNetworks) diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 93991b3b2..58e422f87 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -33,10 +33,10 @@ function App({ Component, pageProps }: NextAppProps) { if ( typeof window !== "undefined" && env && - env["PUBLIC_POSTHOG_ID"] !== undefined + env.PUBLIC_POSTHOG_ID !== undefined ) { - posthog.init(env["PUBLIC_POSTHOG_ID"] as string, { - api_host: env["PUBLIC_POSTHOG_HOST"] as string, + posthog.init(env.PUBLIC_POSTHOG_ID, { + api_host: env.PUBLIC_POSTHOG_HOST, person_profiles: "identified_only", loaded: (posthog) => { if (window.location.hostname.includes("localhost")) { @@ -92,7 +92,7 @@ function App({ Component, pageProps }: NextAppProps) { - {env && env["PUBLIC_VERCEL_ANALYTICS_ENABLED"] && } + {env && env.PUBLIC_VERCEL_ANALYTICS_ENABLED && } ); diff --git a/apps/web/src/pages/block/[id].tsx b/apps/web/src/pages/block/[id].tsx index 2ff21f3d2..1e26ce42a 100644 --- a/apps/web/src/pages/block/[id].tsx +++ b/apps/web/src/pages/block/[id].tsx @@ -58,7 +58,7 @@ const Block: NextPage = function () { const blockNumber = blockData ? blockData.number : undefined; const { env } = useEnv(); - const networkName = env ? (env["PUBLIC_NETWORK_NAME"] as string) : undefined; + const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; const detailsFields: DetailsLayoutProps["fields"] | undefined = useMemo(() => { diff --git a/apps/web/src/providers/Env.tsx b/apps/web/src/providers/Env.tsx index 7e8d17239..a0c5c617c 100644 --- a/apps/web/src/providers/Env.tsx +++ b/apps/web/src/providers/Env.tsx @@ -1,7 +1,18 @@ import React, { createContext, useContext, useState, useEffect } from "react"; +import type { z } from "zod"; + +import type { clientEnvVarsSchema } from "~/env.mjs"; + +type ClientEnvVars = z.infer; + +type ClientEnv = { + [Key in keyof ClientEnvVars as Key extends `NEXT_${infer Rest}` + ? Rest + : never]: ClientEnvVars[Key]; +}; interface EnvContextType { - env?: Record; + env?: ClientEnv; } const EnvContext = createContext({ env: undefined }); @@ -9,8 +20,7 @@ const EnvContext = createContext({ env: undefined }); export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const [env, setEnv] = - useState>(); + const [env, setEnv] = useState(); useEffect(() => { const fetchEnv = async () => {