mirror of
https://github.com/growthbook/growthbook.git
synced 2021-08-07 14:23:53 +03:00
Changes to enable running Growth Book in a docker container (#13)
- Load front-end environment variables at runtime instead of buildtime - Add root Dockerfile and docker-compose.yml - Update CI to push docker images to Docker Hub - Update quick start in README to use `docker-compose up -d` instead of `yarn dev`
This commit is contained in:
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
||||
# Build and test artifacts
|
||||
.git
|
||||
.github
|
||||
node_modules
|
||||
packages/front-end/.next
|
||||
packages/back-end/coverage
|
||||
**/dist
|
||||
**/*.log
|
||||
|
||||
# Docs package
|
||||
packages/docs
|
||||
|
||||
# Secrets / private data
|
||||
**/*.env.local
|
||||
packages/back-end/uploads
|
||||
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@@ -53,45 +53,39 @@ jobs:
|
||||
# All changes in this push
|
||||
FILE_CHANGES=$(git diff --name-only HEAD^ HEAD)
|
||||
|
||||
# See if any relevant back-end changes were made
|
||||
HAS_BACKEND_CHANGES=$(echo "$FILE_CHANGES" | grep -qP 'yarn.lock|packages/back-end' && echo "true" || echo "false")
|
||||
# See if any changes were made to the back-end
|
||||
HAS_BACKEND_CHANGES=$(echo "$FILE_CHANGES" | grep -qP 'yarn.lock|back-end' && echo "true" || echo "false")
|
||||
echo "::set-output name=backend::${HAS_BACKEND_CHANGES}"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
# See if any changes were made to the app (front-end or back-end)
|
||||
HAS_APP_CHANGES=$(echo "$FILE_CHANGES" | grep -qP 'ci.yml|yarn.lock|package.json|Dockerfile|back-end|front-end' && echo "true" || echo "false")
|
||||
echo "::set-output name=app::${HAS_APP_CHANGES}"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.ref == 'refs/heads/main' && steps.changes.outputs.app == 'true'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build, tag, and push image to Docker Hub
|
||||
if: github.ref == 'refs/heads/main' && steps.changes.outputs.app == 'true'
|
||||
env:
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
# Build and push the docker image
|
||||
docker build -t growthbook/growthbook:latest -t growthbook/growthbook:$IMAGE_TAG .
|
||||
docker push growthbook/growthbook
|
||||
|
||||
- name: Configure AWS credentials for Growth Book Cloud
|
||||
if: github.ref == 'refs/heads/main' && steps.changes.outputs.backend == 'true'
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
if: github.ref == 'refs/heads/main' && steps.changes.outputs.backend == 'true'
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
|
||||
- name: Build, tag, and push image to Amazon ECR
|
||||
if: github.ref == 'refs/heads/main' && steps.changes.outputs.backend == 'true'
|
||||
env:
|
||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
ECR_REPOSITORY: api
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
# Build the back-end
|
||||
yarn workspace back-end build
|
||||
|
||||
# Move into the back-end package and add the root yarn.lock
|
||||
cp yarn.lock packages/back-end/yarn.lock
|
||||
cd packages/back-end
|
||||
|
||||
# Build and push the docker image
|
||||
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
|
||||
docker push $ECR_REGISTRY/$ECR_REPOSITORY
|
||||
|
||||
# Cleanup
|
||||
rm yarn.lock
|
||||
|
||||
- name: Deploy to ECS
|
||||
|
||||
- name: Deploy Back-end to ECS for Growth Book Cloud
|
||||
if: github.ref == 'refs/heads/main' && steps.changes.outputs.backend == 'true'
|
||||
run:
|
||||
aws ecs update-service --cluster prod-api --service prod-api --force-new-deployment --region us-east-1
|
||||
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:14-alpine
|
||||
|
||||
WORKDIR /usr/local/src/app
|
||||
|
||||
# Copy only the required files
|
||||
COPY . /usr/local/src/app
|
||||
|
||||
RUN \
|
||||
# Install with dev dependencies
|
||||
yarn install --frozen-lockfile --ignore-optional \
|
||||
# Build the app
|
||||
&& yarn build \
|
||||
# Then do a clean install with only production dependencies
|
||||
&& rm -rf node_modules \
|
||||
&& yarn install --frozen-lockfile --production=true --ignore-optional \
|
||||
# Clear the yarn cache
|
||||
&& yarn cache clean
|
||||
|
||||
CMD ["yarn","start"]
|
||||
40
README.md
40
README.md
@@ -26,28 +26,40 @@ Join [our Growth Book Users Slack community](https://join.slack.com/t/growthbook
|
||||
|
||||
## Requirements
|
||||
|
||||
- NodeJS 12.x or higher
|
||||
- Yarn
|
||||
- Docker (plus docker-compose for running locally)
|
||||
- MongoDB 3.2 or higher
|
||||
- A compatible data source (Snowflake, Redshift, BigQuery, Mixpanel, Postgres, Athena, or Google Analytics)
|
||||
- _(optional)_ An SMTP server for emailing invites, reset password links, etc.
|
||||
- _(optional)_ Google OAuth keys (only if using Google Analytics as a data source)
|
||||
|
||||
Don't want to install, deploy, and maintain Growth Book on your own? Let us do it for you at https://www.growthbook.io
|
||||
We also offer a hosted cloud version that's free to get started: https://app.growthbook.io
|
||||
|
||||
## Dev Quick Start
|
||||
## Quick Start
|
||||
|
||||
1. Start MongoDB locally:
|
||||
```sh
|
||||
docker run -d -p 27017:27017 --name mongo \
|
||||
-e MONGO_INITDB_ROOT_USERNAME=root \
|
||||
-e MONGO_INITDB_ROOT_PASSWORD=password \
|
||||
mongo
|
||||
```
|
||||
2. Run `yarn` to install dependencies
|
||||
3. Run `yarn dev` and visit http://localhost:3000
|
||||
1. Clone this repo: `git clone https://github.com/growthbook/growthbook.git && cd growthbook`
|
||||
2. Start docker-compose: `docker-compose up -d`
|
||||
3. Visit http://localhost:3000
|
||||
|
||||
If you need to change any of the default settings (e.g. to configure an email server or add Google OAuth Keys), copy `packages/back-end/.env.example` to `packages/back-end/.env.local` and edit that file as needed.
|
||||
If you need to change any of the default settings (e.g. to configure an email server or add Google OAuth Keys), edit `docker-compose.yml` and add environment variables for the growthbook service.
|
||||
|
||||
These are all the environment variables you can set:
|
||||
|
||||
- **JWT_SECRET** - Auth signing key (use a long random string)
|
||||
- **ENCRYPTION_KEY** - Data source credential encryption key (use a long random string)
|
||||
- **APP_ORIGIN** - Used for CORS (default set to http://localhost:3000)
|
||||
- **MONGODB_URI** - The MongoDB connection string
|
||||
- **DISABLE_TELEMETRY** - We collect anonymous telemetry data to help us improve Growth Book. Set to "true" to disable.
|
||||
- **API_HOST** - (default set to http://localhost:3100)
|
||||
- Email SMTP Settings:
|
||||
- **EMAIL_ENABLED** ("true" or "false")
|
||||
- **EMAIL_HOST**
|
||||
- **EMAIL_PORT**
|
||||
- **EMAIL_HOST_USER**
|
||||
- **EMAIL_HOST_PASSWORD**
|
||||
- **EMAIL_USE_TLS** ("true" or "false")
|
||||
- Google OAuth Settings (only if using Google Analytics as a data source)
|
||||
- **GOOGLE_OAUTH_CLIENT_ID**
|
||||
- **GOOGLE_OAUTH_CLIENT_SECRET**
|
||||
|
||||
View the full developer docs at https://docs.growthbook.io
|
||||
|
||||
|
||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: "3"
|
||||
services:
|
||||
mongo:
|
||||
image: "mongo:latest"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=password
|
||||
growthbook:
|
||||
image: "growthbook/growthbook:latest"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3100:3100"
|
||||
depends_on:
|
||||
- mongo
|
||||
environment:
|
||||
- MONGODB_URI=mongodb://root:password@mongo:27017/
|
||||
17
package.json
17
package.json
@@ -8,19 +8,15 @@
|
||||
"type-check": "wsrun -m type-check",
|
||||
"test": "wsrun -m test",
|
||||
"dev": "wsrun -p '*-end' -m dev",
|
||||
"dev:docs": "yarn workspace docs dev",
|
||||
"build": "wsrun -m build",
|
||||
"build:back": "yarn workspace back-end build",
|
||||
"build:front": "yarn workspace front-end build",
|
||||
"build:docs": "yarn workspace docs build",
|
||||
"start:back": "yarn workspace back-end start",
|
||||
"start:front": "yarn workspace front-end start",
|
||||
"start:docs": "yarn workspace docs start"
|
||||
"build": "wsrun -p '*-end' -m build",
|
||||
"start": "wsrun -p '*-end' -m start"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"wsrun": "^5.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^10.2.0",
|
||||
"@types/eslint": "^6.1.1",
|
||||
@@ -33,8 +29,7 @@
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.7",
|
||||
"prettier": "^2.2.1",
|
||||
"typescript": "^4.2.4",
|
||||
"wsrun": "^5.2.4"
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
FROM node:alpine
|
||||
|
||||
WORKDIR /usr/local/src/app
|
||||
|
||||
COPY ./dist /usr/local/src/app/dist
|
||||
COPY ./package.json /usr/local/src/app/package.json
|
||||
COPY ./yarn.lock /usr/local/src/app/yarn.lock
|
||||
|
||||
RUN yarn install --frozen-lockfile --production=true --ignore-optional \
|
||||
&& rm -rf /usr/local/share/.cache/yarn
|
||||
|
||||
CMD ["yarn","start"]
|
||||
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"copy-templates": "mkdir -p dist/templates && cp -r src/templates/* dist/templates",
|
||||
"dev": "yarn copy-templates && ts-node-dev src/server.ts",
|
||||
"build": "tsc && rm -rf dist/types && mv dist/src/* dist/ && rm -rf dist/src && yarn copy-templates",
|
||||
"build": "rm -rf dist && tsc && rm -rf dist/types && mv dist/src/* dist/ && rm -rf dist/src && yarn copy-templates",
|
||||
"start": "node dist/server.js",
|
||||
"test": "jest --forceExit --coverage --verbose --detectOpenHandles",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
|
||||
@@ -1 +1 @@
|
||||
NEXT_PUBLIC_API_HOST=http://localhost:3100
|
||||
API_HOST=http://localhost:3100
|
||||
@@ -1,8 +1,6 @@
|
||||
import Auth from "../components/Auth/Auth";
|
||||
import { AuthSource } from "../services/auth";
|
||||
import { getApiHost } from "../services/utils";
|
||||
|
||||
const apiHost = getApiHost();
|
||||
import { getApiHost } from "../services/env";
|
||||
|
||||
let token: string;
|
||||
let createdAt: number;
|
||||
@@ -14,7 +12,7 @@ async function refreshToken(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(apiHost + "/auth/refresh", {
|
||||
const res = await fetch(getApiHost() + "/auth/refresh", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
@@ -67,7 +65,7 @@ const localAuthSource: AuthSource = {
|
||||
logout: async () => {
|
||||
token = "";
|
||||
createdAt = 0;
|
||||
await fetch(apiHost + "/auth/logout", {
|
||||
await fetch(getApiHost() + "/auth/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ReactElement, useState } from "react";
|
||||
import useForm from "../../hooks/useForm";
|
||||
import track from "../../services/track";
|
||||
import { getApiHost } from "../../services/utils";
|
||||
import { getApiHost } from "../../services/env";
|
||||
import Modal from "../Modal";
|
||||
|
||||
const apiHost = getApiHost();
|
||||
|
||||
export default function Auth({
|
||||
onSuccess,
|
||||
}: {
|
||||
@@ -28,7 +26,7 @@ export default function Auth({
|
||||
state === "forgotSuccess"
|
||||
? undefined
|
||||
: async () => {
|
||||
const res = await fetch(apiHost + "/auth/" + state, {
|
||||
const res = await fetch(getApiHost() + "/auth/" + state, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
||||
import clsx from "clsx";
|
||||
import styles from "./SidebarLink.module.scss";
|
||||
import { FiChevronDown } from "react-icons/fi";
|
||||
import { isCloud } from "../../services/utils";
|
||||
import { isCloud } from "../../services/env";
|
||||
|
||||
export type SidebarLinkProps = {
|
||||
name: string;
|
||||
@@ -79,6 +79,12 @@ const SidebarLink: FC<SidebarLinkProps> = (props) => {
|
||||
if (l.superAdmin && !admin) return null;
|
||||
if (l.settingsPermission && !permissions.organizationSettings)
|
||||
return null;
|
||||
if (l.cloudOnly && !isCloud()) {
|
||||
return null;
|
||||
}
|
||||
if (l.selfHostedOnly && isCloud()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
|
||||
@@ -18,7 +18,7 @@ import clsx from "clsx";
|
||||
import styles from "./TopNav.module.scss";
|
||||
import { useRouter } from "next/router";
|
||||
import ChangePasswordModal from "../Auth/ChangePasswordModal";
|
||||
import { isCloud } from "../../services/utils";
|
||||
import { isCloud } from "../../services/env";
|
||||
|
||||
const TopNav: FC<{
|
||||
toggleLeftMenu?: () => void;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApiKeyInterface } from "back-end/types/apikey";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { FaKey, FaPencilAlt } from "react-icons/fa";
|
||||
import { getApiHost, isCloud } from "../../services/utils";
|
||||
import { getApiHost, isCloud } from "../../services/env";
|
||||
import Code from "../Code";
|
||||
import ApiKeysModal from "./ApiKeysModal";
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import Head from "next/head";
|
||||
import { DefinitionsProvider } from "../services/DefinitionsContext";
|
||||
import { useEffect } from "react";
|
||||
import track from "../services/track";
|
||||
import { initEnv } from "../services/env";
|
||||
import { useState } from "react";
|
||||
import LoadingOverlay from "../components/LoadingOverlay";
|
||||
type ModAppProps = AppProps & {
|
||||
Component: { noOrganization?: boolean; preAuth?: boolean };
|
||||
};
|
||||
@@ -17,6 +20,9 @@ function App({
|
||||
pageProps,
|
||||
router,
|
||||
}: ModAppProps): React.ReactElement {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// hacky:
|
||||
const parts = router.route.substr(1).split("/");
|
||||
|
||||
@@ -24,32 +30,53 @@ function App({
|
||||
const preAuth = Component.preAuth || false;
|
||||
|
||||
useEffect(() => {
|
||||
track("App Load");
|
||||
initEnv()
|
||||
.then(() => {
|
||||
setReady(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e.message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) return;
|
||||
track("App Load");
|
||||
}, [ready]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Growth Book</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
</Head>
|
||||
<AuthProvider>
|
||||
<ProtectedPage
|
||||
organizationRequired={organizationRequired}
|
||||
preAuth={preAuth}
|
||||
>
|
||||
{organizationRequired && !preAuth ? (
|
||||
<DefinitionsProvider>
|
||||
<Layout />
|
||||
<main className={`main ${parts[0]}`}>
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
</DefinitionsProvider>
|
||||
) : (
|
||||
<Component {...pageProps} />
|
||||
)}
|
||||
</ProtectedPage>
|
||||
</AuthProvider>
|
||||
{ready ? (
|
||||
<AuthProvider>
|
||||
<ProtectedPage
|
||||
organizationRequired={organizationRequired}
|
||||
preAuth={preAuth}
|
||||
>
|
||||
{organizationRequired && !preAuth ? (
|
||||
<DefinitionsProvider>
|
||||
<Layout />
|
||||
<main className={`main ${parts[0]}`}>
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
</DefinitionsProvider>
|
||||
) : (
|
||||
<Component {...pageProps} />
|
||||
)}
|
||||
</ProtectedPage>
|
||||
</AuthProvider>
|
||||
) : error ? (
|
||||
<div className="container mt-3">
|
||||
<div className="alert alert-danger">
|
||||
Error Initializing Growth Book: {error}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<LoadingOverlay />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
17
packages/front-end/pages/api/init.ts
Normal file
17
packages/front-end/pages/api/init.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// Get env variables at runtime on the front-end while still using SSG
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { API_HOST, IS_CLOUD, DISABLE_TELEMETRY } = process.env;
|
||||
|
||||
res.status(200).json({
|
||||
apiHost: API_HOST || "http://localhost:3100",
|
||||
cloud: !!IS_CLOUD,
|
||||
telemetry:
|
||||
DISABLE_TELEMETRY === "debug"
|
||||
? "debug"
|
||||
: DISABLE_TELEMETRY
|
||||
? "disable"
|
||||
: "enable",
|
||||
});
|
||||
}
|
||||
@@ -3,9 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import LoadingOverlay from "../components/LoadingOverlay";
|
||||
import Modal from "../components/Modal";
|
||||
import { getApiHost, isCloud } from "../services/utils";
|
||||
|
||||
const apiHost = getApiHost();
|
||||
import { getApiHost, isCloud } from "../services/env";
|
||||
|
||||
export default function ResetPasswordPage(): ReactElement {
|
||||
const router = useRouter();
|
||||
@@ -25,7 +23,7 @@ export default function ResetPasswordPage(): ReactElement {
|
||||
}
|
||||
|
||||
// Check if token is valid
|
||||
fetch(apiHost + "/auth/reset/" + token)
|
||||
fetch(getApiHost() + "/auth/reset/" + token)
|
||||
.then((res) => res.json())
|
||||
.then((json: { status: number; message?: string; email?: string }) => {
|
||||
if (json.status > 200) {
|
||||
@@ -57,7 +55,7 @@ export default function ResetPasswordPage(): ReactElement {
|
||||
success || error || loading
|
||||
? undefined
|
||||
: async () => {
|
||||
const res = await fetch(apiHost + "/auth/reset/" + token, {
|
||||
const res = await fetch(getApiHost() + "/auth/reset/" + token, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SettingsApiResponse } from ".";
|
||||
import LoadingOverlay from "../../components/LoadingOverlay";
|
||||
import SubscriptionInfo from "../../components/Settings/SubscriptionInfo";
|
||||
import useApi from "../../hooks/useApi";
|
||||
import { isCloud } from "../../services/utils";
|
||||
import { isCloud } from "../../services/env";
|
||||
|
||||
const BillingPage: FC = () => {
|
||||
const { data, error } = useApi<SettingsApiResponse>(`/organization`);
|
||||
|
||||
@@ -5,9 +5,7 @@ import auth0AuthSource from "../authSources/auth0AuthSource";
|
||||
import localAuthSource from "../authSources/localAuthSource";
|
||||
import { OrganizationInterface } from "back-end/types/organization";
|
||||
import Modal from "../components/Modal";
|
||||
import { getApiHost, isCloud } from "./utils";
|
||||
|
||||
const apiHost = getApiHost();
|
||||
import { getApiHost, isCloud } from "./env";
|
||||
|
||||
export type MemberRole = "collaborator" | "designer" | "developer" | "admin";
|
||||
|
||||
@@ -90,8 +88,6 @@ export interface AuthSource {
|
||||
getJWT: () => Promise<string>;
|
||||
}
|
||||
|
||||
const authSource = isCloud() ? auth0AuthSource : localAuthSource;
|
||||
|
||||
export const AuthProvider: React.FC = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -106,6 +102,8 @@ export const AuthProvider: React.FC = ({ children }) => {
|
||||
const router = useRouter();
|
||||
const initialOrgId = router.query.org ? router.query.org + "" : null;
|
||||
|
||||
const authSource = isCloud() ? auth0AuthSource : localAuthSource;
|
||||
|
||||
useEffect(() => {
|
||||
authSource
|
||||
.init(router)
|
||||
@@ -148,12 +146,8 @@ export const AuthProvider: React.FC = ({ children }) => {
|
||||
>
|
||||
<h3>Error Reaching API</h3>
|
||||
<p>
|
||||
Could not communicate with the Growth Book API at{" "}
|
||||
<code>{getApiHost()}</code>.
|
||||
</p>
|
||||
<p>
|
||||
If you just started the server with <code>yarn dev</code>, wait a
|
||||
minute for the back-end to fully initialize and try again.
|
||||
Could not reach the Growth Book API at <code>{getApiHost()}</code>. Is
|
||||
it running?
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
@@ -195,7 +189,7 @@ export const AuthProvider: React.FC = ({ children }) => {
|
||||
init.headers["X-Organization"] = orgId;
|
||||
}
|
||||
|
||||
const response = await fetch(apiHost + url, init);
|
||||
const response = await fetch(getApiHost() + url, init);
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
|
||||
28
packages/front-end/services/env.ts
Normal file
28
packages/front-end/services/env.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
const env: {
|
||||
telemetry: "debug" | "enable" | "disable";
|
||||
cloud: boolean;
|
||||
apiHost: string;
|
||||
} = {
|
||||
telemetry: "enable",
|
||||
cloud: false,
|
||||
apiHost: "",
|
||||
};
|
||||
|
||||
export async function initEnv() {
|
||||
const res = await fetch("/api/init");
|
||||
const json = await res.json();
|
||||
Object.assign(env, json);
|
||||
}
|
||||
|
||||
export function getApiHost(): string {
|
||||
return env.apiHost;
|
||||
}
|
||||
export function isCloud(): boolean {
|
||||
return env.cloud;
|
||||
}
|
||||
export function isTelemetryEnabled() {
|
||||
return env.telemetry === "enable";
|
||||
}
|
||||
export function inTelemetryDebugMode(): boolean {
|
||||
return env.telemetry === "debug";
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ApiCallType } from "./auth";
|
||||
import { getApiHost } from "./utils";
|
||||
|
||||
const apiHost = getApiHost();
|
||||
import { getApiHost } from "./env";
|
||||
|
||||
export async function uploadFile(
|
||||
apiCall: ApiCallType<{ uploadURL: string; fileURL: string }>,
|
||||
@@ -13,7 +11,7 @@ export async function uploadFile(
|
||||
});
|
||||
|
||||
const res = await fetch(
|
||||
uploadURL.match(/^\//) ? apiHost + uploadURL : uploadURL,
|
||||
uploadURL.match(/^\//) ? getApiHost() + uploadURL : uploadURL,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
@@ -28,6 +26,6 @@ export async function uploadFile(
|
||||
}
|
||||
|
||||
return {
|
||||
fileURL: fileURL.match(/^\//) ? apiHost + fileURL : fileURL,
|
||||
fileURL: fileURL.match(/^\//) ? getApiHost() + fileURL : fileURL,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,27 +6,18 @@ Track anonymous usage statistics
|
||||
- For example, if people start creating a metric and then
|
||||
abandon the form, that tells us the UI needs improvement.
|
||||
- You can disable this tracking completely by setting
|
||||
NEXT_PUBLIC_DISABLE_TELEMETRY=1 in your env.
|
||||
- To console.log the telemetry data instead of sending to us,
|
||||
you can set NEXT_PUBLIC_TELEMETRY_DEBUG=1 in your env.
|
||||
DISABLE_TELEMETRY=1 in your env.
|
||||
*/
|
||||
|
||||
import { jitsuClient, JitsuClient } from "@jitsu/sdk-js";
|
||||
import { isCloud } from "./utils";
|
||||
|
||||
export function isTelemetryEnabled() {
|
||||
return (
|
||||
!process.env.NEXT_PUBLIC_DISABLE_TELEMETRY &&
|
||||
!process.env.NEXT_PUBLIC_TELEMETRY_DEBUG
|
||||
);
|
||||
}
|
||||
import { inTelemetryDebugMode, isCloud, isTelemetryEnabled } from "./env";
|
||||
|
||||
let jitsu: JitsuClient;
|
||||
export default function track(
|
||||
event: string,
|
||||
props: Record<string, unknown> = {}
|
||||
): void {
|
||||
if (process.env.NEXT_PUBLIC_TELEMETRY_DEBUG) {
|
||||
if (inTelemetryDebugMode()) {
|
||||
console.log("Telemetry Event - ", event, props);
|
||||
}
|
||||
if (!isTelemetryEnabled()) return;
|
||||
|
||||
@@ -22,10 +22,3 @@ export function getEvenSplit(n: number) {
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
export function getApiHost(): string {
|
||||
return process.env.NEXT_PUBLIC_API_HOST || "http://localhost:3100";
|
||||
}
|
||||
export function isCloud(): boolean {
|
||||
return !!process.env.NEXT_PUBLIC_IS_CLOUD;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user