mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The rule table mapped HOG_ -> FN_ ahead of any POSTHOG rule, so POSTHOG_FOO split into POST + HOG_ and came out POSTFN_FOO. Nothing sets that. The live pods set INSIGHTS_SKIP_MIGRATION_CHECKS, which bin/migrate-check reads on the container entrypoint, so the misspelling does not degrade a feature -- it runs migrate --check against a database whose history this tree does not share and the container never starts. Both replacements are measured against the shipped tree rather than chosen: every HOG_ name with a counterpart there is spelled INSIGHTS_ (HOG_QL -> INSIGHTS_QL, HOG_FUNCTION_TEMPLATES -> INSIGHTS_FUNCTION_TEMPLATES, 12 of 12 determinate cases), and FN_ appears in it nowhere. Upstream carries 95 HOG_ tokens and zero FN_ ones, so repairing FN_ back out is exactly the inverse of the bad rule and cannot collide with an upstream name. The rule is fixed for the next convergence, and the tree it already produced is repaired in the same commit -- the codemod is idempotent against its own output, so it cannot undo this itself.
341 lines
13 KiB
Bash
Executable File
341 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -e
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
# Automatically restart without asking.
|
|
# this gets around needrestart command halting for user input
|
|
export RESTART_MODE=l
|
|
export INSIGHTS_APP_TAG="${INSIGHTS_APP_TAG:-latest}"
|
|
|
|
# Talk to the user
|
|
echo "Welcome to the single instance Insights installer 🦔"
|
|
echo ""
|
|
echo "⚠️ You REALLY need 8GB or more of memory to run this stack ⚠️"
|
|
echo ""
|
|
echo "Power user or aspiring power user?"
|
|
echo "Check out our docs on deploying Insights! https://hanzo.ai/docs/self-host/deploy/hobby"
|
|
echo ""
|
|
|
|
if [ -n "$1" ]
|
|
then
|
|
export INSIGHTS_APP_TAG=$1
|
|
else
|
|
echo "What version of Insights would you like to install? (We default to 'latest')"
|
|
echo "You can check out available versions here: https://hub.docker.com/r/insights/insights/tags"
|
|
read -r INSIGHTS_APP_TAG_READ
|
|
if [ -z "$INSIGHTS_APP_TAG_READ" ]
|
|
then
|
|
echo "Using default and installing $INSIGHTS_APP_TAG"
|
|
else
|
|
export INSIGHTS_APP_TAG=$INSIGHTS_APP_TAG_READ
|
|
echo "Using provided tag: $INSIGHTS_APP_TAG"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if [ -n "$2" ]
|
|
then
|
|
export DOMAIN=$2
|
|
else
|
|
echo "Let's get the exact domain Insights will be installed on"
|
|
echo "Make sure that you have a Host A DNS record pointing to this instance!"
|
|
echo "This will be used for TLS 🔐"
|
|
echo "ie: test.insights.net (NOT an IP address)"
|
|
read -r DOMAIN
|
|
export DOMAIN=$DOMAIN
|
|
fi
|
|
|
|
echo "Ok we'll set up certs for https://$DOMAIN"
|
|
echo ""
|
|
echo "We will need sudo access so the next question is for you to give us superuser access"
|
|
echo "Please enter your sudo password now:"
|
|
sudo echo ""
|
|
|
|
echo "Thanks! 🙏"
|
|
echo ""
|
|
echo "Ok! We'll take it from here 🚀"
|
|
|
|
echo "Making sure any stack that might exist is stopped"
|
|
sudo -E docker-compose -f docker-compose.yml stop &> /dev/null || true
|
|
|
|
# send log of this install for continued support!
|
|
curl -o /dev/null -L --header "Content-Type: application/json" -d "{
|
|
\"api_key\": \"sTMFPsFhdP1Ssg\",
|
|
\"distinct_id\": \"${DOMAIN}\",
|
|
\"properties\": {\"domain\": \"${DOMAIN}\"},
|
|
\"type\": \"capture\",
|
|
\"event\": \"magic_curl_install_start\"
|
|
}" https://us.i.hanzo.ai/batch/ &> /dev/null
|
|
|
|
# update apt cache
|
|
echo "Grabbing latest apt caches"
|
|
sudo apt update
|
|
|
|
# clone insights
|
|
echo "Installing Insights 🦔 from Github"
|
|
sudo apt install -y git
|
|
|
|
# try to clone - if folder is already there pull latest for that branch
|
|
git clone --filter=blob:none https://github.com/Insights/insights.git &> /dev/null || true
|
|
cd insights
|
|
|
|
if [[ "$INSIGHTS_APP_TAG" = "latest-release" ]]
|
|
then
|
|
git fetch --tags
|
|
latestReleaseTag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
|
|
echo "Checking out latest Insights release: $latestReleaseTag"
|
|
echo "Warning Insights don't create tagged releases anymore. It's way better to use 'latest' than 'latest-release'"
|
|
git checkout "$latestReleaseTag"
|
|
elif [[ "$INSIGHTS_APP_TAG" = "latest" ]]
|
|
then
|
|
echo "Fetching latest changes from origin"
|
|
git fetch origin
|
|
current_branch=$(git branch --show-current)
|
|
if [ -n "$current_branch" ]; then
|
|
echo "Updating branch '$current_branch' to latest from origin"
|
|
git reset --hard "origin/$current_branch"
|
|
else
|
|
echo "On detached HEAD: $(git rev-parse --short HEAD)"
|
|
fi
|
|
echo "Now on commit: $(git rev-parse --short HEAD)"
|
|
elif [[ "$INSIGHTS_APP_TAG" =~ ^[0-9a-f]{40}$ ]]
|
|
then
|
|
echo "Checking out specific commit hash: $INSIGHTS_APP_TAG"
|
|
git checkout "$INSIGHTS_APP_TAG"
|
|
else
|
|
releaseTag="${INSIGHTS_APP_TAG/release-/""}"
|
|
git fetch --tags
|
|
echo "Checking out Insights release: $releaseTag"
|
|
git checkout "$releaseTag"
|
|
fi
|
|
|
|
cd ..
|
|
|
|
if [ -n "$3" ]
|
|
then
|
|
export TLS_BLOCK="acme_ca https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
fi
|
|
|
|
if [ "$REGISTRY_URL" == "" ]
|
|
then
|
|
export REGISTRY_URL="insights/insights"
|
|
fi
|
|
|
|
# Write .env only on a fresh install. On a re-run (e.g. after a partial
|
|
# failure) preserve the existing file, so INSIGHTS_SECRET and
|
|
# ENCRYPTION_SALT_KEYS are not regenerated, as it would invalidate active
|
|
# sessions and corrupt encrypted data on a live instance.
|
|
# Mirrors the FileExists(".env") guard in bin/hobby-installer.
|
|
# Tag changes on an existing instance are handled by bin/upgrade-hobby.
|
|
if [ -f .env ]; then
|
|
echo "Existing .env found, preserving configuration"
|
|
else
|
|
# Optional AI provider keys. Insights's AI features (the Max assistant, SQL/regex assist)
|
|
# run on your own LLM provider key. These are safe to leave blank now — you can add them
|
|
# to .env later and apply with bin/upgrade-hobby. An env var set before running this
|
|
# script (e.g. ANTHROPIC_API_KEY=... ./deploy-hobby) is used as-is without prompting.
|
|
# Only prompt on an interactive terminal — a piped/non-interactive install
|
|
# (CI, `curl | bash`) skips the prompts and relies on the env vars instead.
|
|
# Read silently (-s) — these are secrets and shouldn't echo to the terminal/scrollback.
|
|
if [ -t 0 ] && [ -z "$ANTHROPIC_API_KEY" ]; then
|
|
echo ""
|
|
echo "Insights AI runs on an Anthropic API key."
|
|
echo "Enter one to enable Insights AI now, or leave blank to skip (you can add it later):"
|
|
read -rs ANTHROPIC_API_KEY
|
|
echo
|
|
fi
|
|
if [ -t 0 ] && [ -z "$OPENAI_API_KEY" ]; then
|
|
echo "Optionally enter an OpenAI API key to enable SQL and regex AI assist, or leave blank to skip:"
|
|
read -rs OPENAI_API_KEY
|
|
echo
|
|
fi
|
|
|
|
INSIGHTS_SECRET=$(head -c 28 /dev/urandom | sha224sum -b | head -c 56)
|
|
ENCRYPTION_SALT_KEYS=$(openssl rand -hex 16)
|
|
# Dedicated browserless token so a compromise of the rendering container can't yield
|
|
# Django's SECRET_KEY (the compose file falls back to INSIGHTS_SECRET when unset, for
|
|
# installs whose .env predates this variable).
|
|
BROWSERLESS_SECRET=$(openssl rand -hex 32)
|
|
cat > .env <<EOF
|
|
INSIGHTS_SECRET=$INSIGHTS_SECRET
|
|
ENCRYPTION_SALT_KEYS=$ENCRYPTION_SALT_KEYS
|
|
BROWSERLESS_SECRET=$BROWSERLESS_SECRET
|
|
DOMAIN=$DOMAIN
|
|
TLS_BLOCK=$TLS_BLOCK
|
|
REGISTRY_URL=$REGISTRY_URL
|
|
CADDY_TLS_BLOCK=$TLS_BLOCK
|
|
CADDY_HOST="$DOMAIN, http://, https://"
|
|
INSIGHTS_APP_TAG=$INSIGHTS_APP_TAG
|
|
EOF
|
|
# Only persist AI keys that were actually provided — skip writing a blank
|
|
# ANTHROPIC_API_KEY= line that would just clutter .env with an empty assignment.
|
|
# (A blank value is harmless either way: bin/upgrade-hobby greps for
|
|
# ^ANTHROPIC_API_KEY=.+, so an empty value counts as missing and the user is
|
|
# still offered the key on the next upgrade.)
|
|
[ -n "$ANTHROPIC_API_KEY" ] && echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env
|
|
[ -n "$OPENAI_API_KEY" ] && echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
|
|
fi
|
|
|
|
# Download GeoLite2-City.mmdb if it doesn't exist
|
|
echo "Downloading GeoIP database file"
|
|
sudo apt-get update &&
|
|
sudo apt-get install -y --no-install-recommends curl ca-certificates brotli &&
|
|
sudo mkdir -p ./share &&
|
|
if [ ! -f ./share/GeoLite2-City.mmdb ]; then
|
|
curl -L 'https://mmdbcdn.insights.net/' --http1.1 | brotli --decompress | sudo tee ./share/GeoLite2-City.mmdb > /dev/null &&
|
|
echo '{"date": "'"$(date +%Y-%m-%d)"'"}' | sudo tee ./share/GeoLite2-City.json > /dev/null
|
|
sudo chmod 644 ./share/GeoLite2-City.mmdb &&
|
|
sudo chmod 644 ./share/GeoLite2-City.json
|
|
fi
|
|
|
|
# write entrypoint
|
|
# NOTE: this is duplicated in bin/upgrade-hobby, so if you change it here,
|
|
# change it there too.
|
|
rm -rf compose
|
|
mkdir -p compose
|
|
cat > compose/start <<EOF
|
|
#!/bin/bash
|
|
./compose/wait
|
|
./bin/migrate
|
|
./bin/docker-server
|
|
EOF
|
|
chmod +x compose/start
|
|
|
|
cat > compose/temporal-django-worker <<EOF
|
|
#!/bin/bash
|
|
./bin/temporal-django-worker
|
|
EOF
|
|
chmod +x compose/temporal-django-worker
|
|
|
|
# write wait script
|
|
cat > compose/wait <<EOF
|
|
#!/usr/bin/env python3
|
|
|
|
import socket
|
|
import time
|
|
|
|
def loop():
|
|
print("Waiting for Datastore and Postgres to be ready")
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.connect(('datastore', 9000))
|
|
print("Datastore is ready")
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.connect(('db', 5432))
|
|
print("Postgres is ready")
|
|
except ConnectionRefusedError as e:
|
|
time.sleep(5)
|
|
loop()
|
|
|
|
loop()
|
|
EOF
|
|
chmod +x compose/wait
|
|
|
|
# setup docker
|
|
if ! command -v docker &> /dev/null; then
|
|
echo "Docker is not installed. Setting up Docker."
|
|
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
|
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo -E apt-key add -
|
|
sudo add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu jammy stable"
|
|
sudo apt update
|
|
sudo apt-cache policy docker-ce
|
|
sudo apt install -y docker-ce containerd.io=2.2.0-2~ubuntu.22.04~jammy
|
|
else
|
|
echo "Docker is already installed. Skipping installation."
|
|
fi
|
|
|
|
# setup docker-compose
|
|
echo "Setting up Docker Compose"
|
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.33.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose || true
|
|
sudo chmod +x /usr/local/bin/docker-compose
|
|
|
|
# enable docker without sudo
|
|
sudo usermod -aG docker "${USER}" || true
|
|
|
|
# start up the stack
|
|
echo "Configuring Docker Compose...."
|
|
rm -f docker-compose.yml
|
|
cp insights/docker-compose.base.yml docker-compose.base.yml
|
|
cp insights/docker-compose.hobby.yml docker-compose.yml
|
|
cp insights/.env.services .env.services
|
|
echo "Starting the stack!"
|
|
# Retry docker-compose up with --pull always up to 3 times
|
|
for attempt in 1 2 3; do
|
|
echo "Starting stack (attempt $attempt/3)..."
|
|
if sudo -E docker-compose -f docker-compose.yml up -d --no-build --pull always; then
|
|
echo "Stack started successfully"
|
|
break
|
|
else
|
|
if [ $attempt -lt 3 ]; then
|
|
echo "Failed to start stack, waiting 30s before retry..."
|
|
sleep 30
|
|
else
|
|
echo "Failed to start stack after 3 attempts"
|
|
exit 1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [ -z "$SKIP_HEALTH_CHECK" ]; then
|
|
# Normal hobby deployment: wait for health check
|
|
echo "We will need to wait ~5-10 minutes for things to settle down, migrations to finish, and TLS certs to be issued"
|
|
echo ""
|
|
echo "⏳ Waiting for Insights web to boot (this will take a few minutes)"
|
|
echo " (timeout after 10 minutes if app doesn't start)"
|
|
|
|
# Wait for health check with 600 second timeout
|
|
# Track the start time to measure actual wait time
|
|
HEALTH_START=$(date +%s)
|
|
# shellcheck disable=SC2016 # Single quotes intentional - %{http_code} is curl format string, not shell variable
|
|
if timeout 600 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost/_health)" != "200" ]]; do sleep 5; done'; then
|
|
HEALTH_END=$(date +%s)
|
|
HEALTH_DURATION=$((HEALTH_END - HEALTH_START))
|
|
echo "⌛️ Insights looks up! (after ${HEALTH_DURATION} seconds)"
|
|
echo ""
|
|
echo "🎉🎉🎉 Done! 🎉🎉🎉"
|
|
# send log of this install for continued support!
|
|
curl -o /dev/null -L --header "Content-Type: application/json" -d "{
|
|
\"api_key\": \"sTMFPsFhdP1Ssg\",
|
|
\"distinct_id\": \"${DOMAIN}\",
|
|
\"properties\": {\"domain\": \"${DOMAIN}\"},
|
|
\"type\": \"capture\",
|
|
\"event\": \"magic_curl_install_complete\"
|
|
}" https://us.i.hanzo.ai/batch/ &> /dev/null
|
|
else
|
|
HEALTH_EXIT=$?
|
|
echo ""
|
|
if [ $HEALTH_EXIT -eq 124 ]; then
|
|
echo "❌ Health check timed out after 10 minutes"
|
|
echo " The application did not start within the expected timeframe"
|
|
else
|
|
echo "❌ Health check failed with exit code: $HEALTH_EXIT"
|
|
fi
|
|
echo ""
|
|
echo "Please check the logs with 'docker-compose logs' for more details."
|
|
echo "Common issues:"
|
|
echo " - Docker image pull failed: 'docker logs <container>'"
|
|
echo " - Database migration stuck: 'docker-compose logs db datastore'"
|
|
echo " - Insufficient memory: 'free -h' and 'top'"
|
|
exit 1
|
|
fi
|
|
else
|
|
# CI mode: skip health check, exit immediately after docker-compose up
|
|
echo "⏭️ Skipping health check (SKIP_HEALTH_CHECK is set)"
|
|
echo "Stack started, containers are running in background"
|
|
fi
|
|
|
|
echo ""
|
|
echo "To stop the stack run 'docker-compose stop'"
|
|
echo "To start the stack again run 'docker-compose start'"
|
|
echo "If you have any issues at all delete everything in this directory and run the curl command again"
|
|
echo ""
|
|
# shellcheck disable=SC2016 # we don't want to expand this expression
|
|
echo 'To upgrade: run /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/insights/insights/HEAD/bin/upgrade-hobby)"'
|
|
echo ""
|
|
echo "Insights will be up at the location you provided!"
|
|
echo "https://${DOMAIN}"
|
|
echo ""
|
|
echo "It's dangerous to go alone! Take this: 🦔"
|