Flevo CFD

How Read Environment Variables in Single Page Applications

Recetly I’ve been working on adding Sentry to our UI which is a Single Page Application. We’re using systemd and podman to run our services.

to initialize Sentry we need some configuration which needs to be passed at the runtime but the SPA is a static javascript codebase which can not have access to environment variables.

To tackle it I came across a few solutions.

#1

We can use placeholders in the Sentry initialization code and replace them with actual values in the service file.

This is the systemd service file for the UI service. the UI is a bunch of static javascript code which nginx serves them. the required environment variables for this container is passed as a file

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Unit]
...

[Service]
...
ExecStart=podman run \
    ...
    --env-file env.conf \
    ...
    nginx -c nginx.conf

[Install]
...

This is the code that initialize Sentry

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const sentryDsn = "#SENTRY_DSN#";
const isSentryDsnReplaced = !sentryDsn.startsWith("#");
if (isSentryDsnReplaced) {
    Sentry.init({
        app,
        dsn: sentryDsn,
        release: "#SENTRY_RELEASE#",
        environment: "#SENTRY_ENVIRONMENT#",
        ...
    });
}

We add a post command which runs if ExecStart is successfull. it replaces the placeholders with the actual values.

1
2
3
ExecStartPost=podman exec ui bash -c 'find /html/ -type f -print0 | xargs -0 sed -i "s|#SENTRY_DSN#|$SENTRY_DSN|g"'
ExecStartPost=podman exec ui bash -c 'find /html/ -type f -print0 | xargs -0 sed -i "s|#SENTRY_RELEASE#|$SENTRY_RELEASE|g"'
ExecStartPost=podman exec ui bash -c 'find /html/ -type f -print0 | xargs -0 sed -i "s|#SENTRY_ENVIRONMENT#|$SENTRY_ENVIRONMENT|g"'

As it seems, it’s a hacky way. I usually ask the following questions to see if the solution fits

  1. Will the new developers that join the project, understand it on the first go?
  2. Is it a standard or close-to-standard way of implementing this requirement?
  3. Do we need to be concered about the future changes for this requirement?

Note that the the command is wapped with single quotes which makes bash to load the variables from within the container, not the machine that runs the command which is the host.

#2

The next solution is to write the configuration in a javacsript file and include it in the main page of the app which is index.html.

1
2
3
4
5
6
7
8
9
<head>
    ...
    <script src="/config.js"></script>
    ...
</head>
<body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
</body>

In the code

1
2
3
4
5
6
7
8
9
if (window.sentryDsn) {
    Sentry.init({
        app,
        dsn: window.sentryDsn,
        release: window.sentryRelease,
        environment: window.sentryEnvironment,
        ...
    });
}

This is not also a good solution as it messes with the global state. you really don’t know which configuration is defined in the global window object and which is not.

#3

The last solution is to write configuration in a separete file and fetch it in the code.

We need to add an additional step in the build process as below.

1
echo "{\"dsn\": \"$SENTRY_DSN\", \"environment\": \"$SENTRY_ENVIRONMENT\", \"release\": \"$SENTRY_RELEASE\"}" > build/sentry.config.json; 

In the code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const response = await fetch('/sentry.config.json');
if (!response.ok) {
    console.log("Can't load Sentry config");
    return;
}
const sentryConfig = await response.json();
const isSentryConfigValid = sentryConfig.dsn && sentryConfig.release && sentryConfig.environment;
if (isSentryConfigValid) {
    Sentry.init({
        app,
        dsn: sentryConfig.dsn,
        release: sentryConfig.release,
        environment: sentryConfig.environment,
        ...
    });
} else {
    console.log('Sentry has not been initialized.');
}

This way an extra requests is sent on each page refresh which I don’t think is big deal, or is it?

I found the last one, the best.