Skip to content

Commit 3ff8848

Browse files
nbudinclaude
andcommitted
Move env var JS globals into /client_configuration
Removes the inline <script> block from _head.html.erb that set window.intercodeAssetsHost, window.sentryFrontendDSN, and window.rollbarClientAccessToken. All three values now come from the /client_configuration JSON endpoint, which the SPA already fetches at boot. - assets_host: set on window.intercodeAssetsHost after bootstrap resolves, so lazy route chunks still use the CDN; initial chunks on cold load fall back to the origin server (which serves /packs) - sentry_frontend_dsn / rollbar_client_access_token: passed as constructor args to ErrorReporting rather than read from window; AppRoot reads them via useRouteLoaderData('root') This is the first step toward serving a fully static HTML shell for all SPA routes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 039d68e commit 3ff8848

9 files changed

Lines changed: 35 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ Intercode is a convention management system built with:
2626
- **TypeScript**: Run `yarn run tsc --noEmit` after making changes
2727
- **Ruby**: Run the relevant test suite before committing
2828

29+
## Deployment Model
30+
31+
Intercode is shipped as a Docker container with the frontend assets **pre-built at image build time**. Environment variables (including `ASSETS_HOST`, `SENTRY_FRONTEND_DSN`, `ROLLBAR_CLIENT_ACCESS_TOKEN`, etc.) are injected at **container runtime**, not at build time. This means:
32+
33+
- Do not use Vite `define`, `import.meta.env`, or any other build-time constant substitution for runtime env vars.
34+
- Runtime configuration must be delivered to the browser via a server-rendered response (e.g. the `GET /client_configuration` endpoint) or a server-rendered HTML attribute, never baked into the JS bundle.
35+
2936
## Testing Requirements
3037

3138
Whenever changing signup-related functionality (signup services, ranked choice, waitlists, etc.), always add or update tests in the relevant test file under `test/services/` or `test/models/`.

app/controllers/client_configuration_controller.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ def show
1616
oidc_issuer_url: oidc_issuer_url,
1717
rails_default_active_storage_service_name: Rails.application.config.active_storage.service.to_s,
1818
rails_direct_uploads_url: rails_direct_uploads_url,
19-
recaptcha_site_key: Recaptcha.configuration.site_key
19+
recaptcha_site_key: Recaptcha.configuration.site_key,
20+
assets_host: ENV["ASSETS_HOST"].presence,
21+
sentry_frontend_dsn: ENV["SENTRY_FRONTEND_DSN"].presence,
22+
rollbar_client_access_token: ENV["ROLLBAR_CLIENT_ACCESS_TOKEN"].presence
2023
}
2124
end
2225
end

app/javascript/@types/globals.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
interface Window {
22
intercodeAssetsHost?: string;
3-
sentryFrontendDSN?: string;
4-
rollbarClientAccessToken?: string;
53
__intercodeAssetURL: (filename: string) => string;
64
}

app/javascript/AppContexts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export type ClientConfiguration = {
1313
rails_default_active_storage_service_name: string;
1414
rails_direct_uploads_url: string;
1515
recaptcha_site_key: string | null;
16+
assets_host: string | null;
17+
sentry_frontend_dsn: string | null;
18+
rollbar_client_access_token: string | null;
1619
};
1720

1821
export const authenticityTokensManagerContext = createContext<AuthenticityTokensManager>();

app/javascript/AppRoot.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Suspense, useMemo, useState, useEffect, useRef } from 'react';
2-
import { useLocation, useLoaderData, Outlet, useNavigation } from 'react-router';
2+
import { useLocation, useLoaderData, useRouteLoaderData, Outlet, useNavigation } from 'react-router';
33
import { Settings } from 'luxon';
44
import { PageLoadingIndicator } from '@neinteractiveliterature/litform';
55

@@ -12,6 +12,7 @@ import { LazyStripeContext } from './LazyStripe';
1212
import { Stripe } from '@stripe/stripe-js';
1313
import { reloadOnAppEntrypointHeadersMismatch } from './checkAppEntrypointHeadersMatch';
1414
import { initErrorReporting } from 'ErrorReporting';
15+
import { RootLoaderData } from 'root';
1516

1617
export function buildAppRootContextValue(
1718
data: AppRootQueryData | null | undefined,
@@ -81,10 +82,15 @@ function AppRoot(): React.JSX.Element {
8182
const navigationBarRef = useRef<HTMLElement>(null);
8283

8384
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null);
85+
const rootLoaderData = useRouteLoaderData('root') as RootLoaderData | undefined;
8486

8587
useEffect(() => {
86-
initErrorReporting(data.currentUser?.id);
87-
}, [data?.currentUser?.id]);
88+
initErrorReporting(
89+
data.currentUser?.id,
90+
rootLoaderData?.clientConfiguration?.sentry_frontend_dsn,
91+
rootLoaderData?.clientConfiguration?.rollbar_client_access_token,
92+
);
93+
}, [data?.currentUser?.id, rootLoaderData?.clientConfiguration]);
8894

8995
useEffect(() => {
9096
reloadOnAppEntrypointHeadersMismatch();

app/javascript/ErrorReporting.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ class ErrorReporting {
2525
return ErrorReporting.instance!;
2626
}
2727

28-
constructor(currentUserId: string | undefined) {
29-
if (window.sentryFrontendDSN) {
28+
constructor(currentUserId: string | undefined, sentryDsn?: string | null, rollbarToken?: string | null) {
29+
if (sentryDsn) {
3030
import('@sentry/browser').then((Sentry) => {
31-
const instance = Sentry.init({ dsn: window.sentryFrontendDSN });
31+
const instance = Sentry.init({ dsn: sentryDsn });
3232
if (instance != null) {
3333
this.sentry = {
3434
instance,
@@ -42,10 +42,10 @@ class ErrorReporting {
4242
});
4343
}
4444

45-
if (window.rollbarClientAccessToken) {
45+
if (rollbarToken) {
4646
import('rollbar').then((Rollbar) => {
4747
this.rollbar = Rollbar.default.init({
48-
accessToken: window.rollbarClientAccessToken,
48+
accessToken: rollbarToken,
4949
captureUncaught: true,
5050
captureUnhandledRejections: true,
5151
captureIp: 'anonymize',

app/javascript/packs/application.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ const bootstrapPromise: Promise<Bootstrap> = (async () => {
6262
window.location.href = redirectUrl.toString();
6363
});
6464

65+
// Make the CDN host available for lazy chunk loading (renderBuiltUrl reads this at call time).
66+
if (clientConfiguration.assets_host) {
67+
window.intercodeAssetsHost = clientConfiguration.assets_host;
68+
}
69+
6570
return { clientConfiguration, authenticityTokensManager, authManager, client };
6671
})();
6772

@@ -73,6 +78,7 @@ function DataModeApplicationEntry() {
7378
createBrowserRouter(
7479
[
7580
{
81+
id: 'root',
7682
lazy: () => import('root'),
7783
children: appRootRoutes,
7884
},

app/javascript/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import AuthenticityTokensManager, { AuthenticityTokensContext } from 'Authentici
1111
import { StrictMode } from 'react';
1212
import { LoaderFunction, useLoaderData } from 'react-router';
1313

14-
type RootLoaderData = {
14+
export type RootLoaderData = {
1515
clientConfiguration: ClientConfiguration;
1616
authenticityTokensManager: AuthenticityTokensManager;
1717
client: ApolloClient;

app/views/layouts/_head.html.erb

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
22
<title><%= page_title %></title>
3-
<script type="application/javascript">
4-
<% if ENV['ASSETS_HOST'].present? -%>
5-
window.intercodeAssetsHost = <%=raw ENV['ASSETS_HOST'].to_json %>;
6-
<% end -%>
7-
<% if ENV['SENTRY_FRONTEND_DSN'].present? -%>
8-
window.sentryFrontendDSN = <%=raw ENV['SENTRY_FRONTEND_DSN'].to_json %>;
9-
<% end -%>
10-
<% if ENV["ROLLBAR_CLIENT_ACCESS_TOKEN"].present? -%>
11-
window.rollbarClientAccessToken = <%=raw ENV['ROLLBAR_CLIENT_ACCESS_TOKEN'].to_json %>;
12-
<% end -%>
13-
</script>
143
<% if Rails.env.development? %>
154
<script type="module">
165
import RefreshRuntime from <%=raw url_with_possible_host("/@react-refresh", ENV['ASSETS_HOST']).to_json %>

0 commit comments

Comments
 (0)