From 269de76c36fed60f64b5f03a43c4785147c5e2c4 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 27 Aug 2026 14:49:03 +0000 Subject: [PATCH] feat: Offer to send an anonymous failure report in the install script (#37016) Co-authored-by: Claude Fable 5 --- docker/get-n8n.sh | 58 +++++++++++++++++++++++++++++++++++++++--- docker/test-get-n8n.sh | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/docker/get-n8n.sh b/docker/get-n8n.sh index e55f51d7415..c11c52e4b72 100755 --- a/docker/get-n8n.sh +++ b/docker/get-n8n.sh @@ -12,7 +12,7 @@ # Source: https://github.com/n8n-io/n8n/blob/master/docker/get-n8n.sh set -eu -SCRIPT_VERSION="1.2.0" +SCRIPT_VERSION="1.3.0" # The version to install is derived from the latest stable GitHub release in # resolve_n8n_version(); this fallback only applies when that lookup fails. FALLBACK_N8N_VERSION="2.32.0" @@ -31,6 +31,19 @@ UPGRADE=0 NO_START=0 REQUESTED_VERSION="" +# Anonymous failure report (RudderStack): offered only when an install or +# upgrade fails, and sent only after the user answers yes. The payload is the +# script version, OS name, install|upgrade mode and the failed step — nothing +# else. The write key is a public client-side key. DO_NOT_TRACK=1 disables +# the offer entirely. +TELEMETRY_URL="https://nnrry.dataplane.rudderstack.com/v1/track" +TELEMETRY_WRITE_KEY="3IDNZwfcE0lfjVDb0Y1wp8ZVGlg" +# Set before steps whose failure could be on n8n's side (stack definition +# download, image pulls, container start, health check). Failures of the +# user's environment (no docker, port taken, ...) leave it empty and no +# report is offered. +STEP="" + # Apply ANSI styling only when stdout is a terminal that supports it and the # user hasn't opted out (https://no-color.org). if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then @@ -53,8 +66,36 @@ fi say() { printf '%s\n' "$*"; } ok() { printf '%s\342\234\224%s %s\n' "$GREEN" "$RESET" "$*"; } + +# Never sends without an explicit yes. The prompt uses /dev/tty because +# 'curl | sh' owns stdin; without a terminal there is no prompt and no report. +offer_failure_report() { + [ -n "$STEP" ] || return 0 + [ -z "${DO_NOT_TRACK:-}" ] || return 0 + printf '\nSend an anonymous failure report to n8n, so we can fix this?\n(script version, OS name, failed step — nothing else) [y/N] ' >/dev/tty 2>/dev/null || return 0 + read -r answer /dev/null || return 0 + case "$answer" in + [Yy]*) ;; + *) return 0 ;; + esac + mode="install" + [ "$UPGRADE" -eq 1 ] && mode="upgrade" + body="{\"anonymousId\":\"$(gen_secret)\",\"event\":\"install_failed\",\"properties\":{\"scriptVersion\":\"${SCRIPT_VERSION}\",\"os\":\"$(uname -s)\",\"mode\":\"${mode}\",\"step\":\"${STEP}\"}}" + if command -v curl >/dev/null 2>&1; then + curl -s -o /dev/null --max-time 5 -u "${TELEMETRY_WRITE_KEY}:" \ + -H 'Content-Type: application/json' -d "$body" "$TELEMETRY_URL" 2>/dev/null || true + elif command -v wget >/dev/null 2>&1; then + wget -q -O /dev/null -T 5 \ + --header="Authorization: Basic $(printf '%s:' "$TELEMETRY_WRITE_KEY" | base64)" \ + --header='Content-Type: application/json' \ + --post-data="$body" "$TELEMETRY_URL" 2>/dev/null || true + fi + printf 'Thank you!\n' >/dev/tty 2>/dev/null || true +} + fail() { printf '%sError:%s %s\n' "$RED" "$RED_RESET" "$*" >&2 + offer_failure_report exit 1 } @@ -77,6 +118,7 @@ Options: Environment: N8N_DIR Install directory (default: ./n8n) + DO_NOT_TRACK=1 Never offer to send an anonymous failure report. For production-grade setups (TLS, Postgres, queue mode) see: ${DOCS_HOSTING_URL} @@ -312,6 +354,7 @@ compose() { check_rate_limit() { grep -qiE 'toomanyrequests|rate ?limit' "$1" || return 0 rm -f "$1" + STEP="docker-hub-rate-limit" fail "Docker Hub pull rate limit reached — your configuration in ${N8N_DIR} is unaffected. Wait about an hour, then start n8n with: docker compose -f ${N8N_DIR}/compose.yml up -d @@ -374,7 +417,9 @@ do_upgrade() { fi say "Pulling images..." + STEP="image-pull" compose_pull + STEP="container-start" compose_checked up -d --quiet-pull ok "Restarted with n8n ${target}" } @@ -425,6 +470,7 @@ main() { if [ -f "${N8N_DIR}/compose.yml" ] || [ -f "${N8N_DIR}/.env" ]; then if [ "$UPGRADE" -eq 1 ]; then do_upgrade + STEP="n8n-readiness-timeout" wait_for_n8n || fail "n8n did not become ready — check 'docker compose -f ${N8N_DIR}/compose.yml logs n8n'." print_summary exit 0 @@ -434,10 +480,11 @@ main() { if http_get "http://127.0.0.1:${N8N_PORT}/healthz"; then say "n8n is already running at: http://localhost:${N8N_PORT}" else - say "To start it: docker compose -f ${N8N_DIR}/compose.yml up -d" + say "To start it: docker compose -f ${N8N_DIR}/compose.yml up -d" say "Once started, n8n runs at: http://localhost:${N8N_PORT}" fi - say "To upgrade: curl -fsSL https://get.n8n.io | sh -s -- --upgrade" + say "To upgrade: curl -fsSL https://get.n8n.io | sh -s -- --upgrade" + say "To uninstall: docker compose -f ${N8N_DIR}/compose.yml down -v && rm -rf ${N8N_DIR} # DELETES all n8n data" check_compose_freshness exit 0 fi @@ -454,7 +501,9 @@ main() { INSTALL_VERSION="${REQUESTED_VERSION:-$(resolve_n8n_version)}" mkdir -p "${N8N_DIR}" + STEP="stack-definition-download" write_compose + STEP="" ok "Created ${N8N_DIR}/compose.yml" write_searxng_settings ok "Created ${N8N_DIR}/searxng-settings.yml" @@ -469,9 +518,12 @@ main() { fi say "Pulling images (this can take a few minutes on first run)..." + STEP="image-pull" compose_pull + STEP="container-start" compose_checked up -d --quiet-pull ok "Started n8n ${INSTALL_VERSION} and sandbox services" + STEP="n8n-readiness-timeout" wait_for_n8n || fail "n8n did not become ready — check 'docker compose -f ${N8N_DIR}/compose.yml logs n8n'." print_summary } diff --git a/docker/test-get-n8n.sh b/docker/test-get-n8n.sh index 6247c0e9766..b3a7c50ab9c 100644 --- a/docker/test-get-n8n.sh +++ b/docker/test-get-n8n.sh @@ -17,6 +17,8 @@ SCRIPT="$(cd "$(dirname "$0")" && pwd)/get-n8n.sh" # on master — the harness must test this branch's compose file. COMPOSE_SRC="$(cd "$(dirname "$0")" && pwd)/get-n8n-compose.yml" export N8N_COMPOSE_URL="$COMPOSE_SRC" +# Failure-path tests must not prompt for (or send) a failure report. +export DO_NOT_TRACK=1 E2E=0 [ "${1:-}" = "--e2e" ] && E2E=1 @@ -61,6 +63,8 @@ sh "$SCRIPT" --version | grep -q '^get-n8n.sh v' && pass "--version prints scrip fail "--version prints script version" sh "$SCRIPT" --help | grep -q 'Usage:' && pass "--help prints usage" || fail "--help prints usage" +sh "$SCRIPT" --help | grep -q 'DO_NOT_TRACK' && pass "--help documents DO_NOT_TRACK" || + fail "--help documents DO_NOT_TRACK" check_not "unknown flag fails" sh "$SCRIPT" --bogus # fresh --no-start install @@ -103,6 +107,8 @@ rerun_out="$(env N8N_DIR="$WORK/a" sh "$SCRIPT" 2>&1)" && pass "re-run on existi fail "re-run leaves files untouched" echo "$rerun_out" | grep -q 'http://localhost:5678' && pass "re-run tells the user where n8n runs" || fail "re-run tells the user where n8n runs" +echo "$rerun_out" | grep -q 'To uninstall: docker compose' && pass "re-run shows the uninstall command" || + fail "re-run shows the uninstall command" # stack definition versioning [ -n "$(sed -n 's/^# compose-version: *//p' "$WORK/a/compose.yml")" ] && @@ -171,6 +177,52 @@ ratelimit_out="$(env PATH="$WORK/shim:$PATH" N8N_DIR="$WORK/ratelimit" sh "$SCRI echo "$ratelimit_out" | grep -q 'pull rate limit reached' && pass "rate-limit failure prints recovery advice" || fail "rate-limit failure prints recovery advice" +# failure-report consent: a reportable failure on a terminal must prompt, send +# the failed step only on an explicit yes, and send nothing on the default No. +# Needs a pty, so run the script under script(1); its syntax differs between +# util-linux and BSD/macOS, and Git Bash has neither. The curl shim records +# telemetry calls and fails everything else the way an offline curl would. +if [ "$WINDOWS" -eq 0 ] && command -v script >/dev/null 2>&1; then + cat >"$WORK/shim/curl" <>"$WORK/consent.log" ;; +esac +exit 6 +EOF + chmod +x "$WORK/shim/curl" + run_with_tty() { # run_with_tty + answer="$1" + shift + if script --version 2>/dev/null | grep -q util-linux; then + { + sleep 1 + printf '%s\n' "$answer" + sleep 4 + } | script -qec "$*" /dev/null + else + { + sleep 1 + printf '%s\n' "$answer" + sleep 4 + } | script -q /dev/null "$@" + fi + } + rm -f "$WORK/consent.log" + run_with_tty y env "PATH=$WORK/shim:$PATH" DO_NOT_TRACK= N8N_DIR="$WORK/ratelimit" \ + sh "$SCRIPT" --upgrade --version 2.32.0 >/dev/null 2>&1 + grep -q '"event":"install_failed".*"step":"docker-hub-rate-limit"' "$WORK/consent.log" 2>/dev/null && + pass "consented failure report carries the failed step" || + fail "consented failure report carries the failed step" + rm -f "$WORK/consent.log" + run_with_tty '' env "PATH=$WORK/shim:$PATH" DO_NOT_TRACK= N8N_DIR="$WORK/ratelimit" \ + sh "$SCRIPT" --upgrade --version 2.32.0 >/dev/null 2>&1 + [ ! -s "$WORK/consent.log" ] && pass "declined failure report sends nothing" || + fail "declined failure report sends nothing" +else + skip "failure-report consent tests (need script(1) for a pty)" +fi + # truncated download must execute nothing mkdir -p "$WORK/trunc" && cd "$WORK/trunc" || exit 1 head -c 1000 "$SCRIPT" | sh >/dev/null 2>&1