Просмотр исходного кода

Upload browser source maps to PostHog so error stack traces show the real file and line (#510)

* Upload browser source maps to PostHog so error stack traces show the real file and line

* Retry the source map upload on every image workflow run instead of reusing a cached result
Bernt Christian Egeland 2 дней назад
Родитель
Сommit
96853e67d2
5 измененных файлов с 162 добавлено и 0 удалено
  1. 7 0
      .github/workflows/docker-dev.yml
  2. 8 0
      .github/workflows/docker-publish.yml
  3. 23 0
      Dockerfile
  4. 4 0
      next.config.ts
  5. 120 0
      scripts/upload-source-maps.sh

+ 7 - 0
.github/workflows/docker-dev.yml

@@ -55,11 +55,18 @@ jobs:
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
+          # Staging reports to the same PostHog project as production, so its
+          # source maps go there too (see scripts/upload-source-maps.sh).
           build-args: |
             APP_VERSION=dev-${{ steps.sha.outputs.short }}
             NEXT_ACTIONS_FINGERPRINT=${{ steps.actions_key.outputs.id }}
+            POSTHOG_SOURCEMAPS=${{ secrets.POSTHOG_SOURCEMAPS_API_KEY != '' && 'on' || 'off' }}
+            POSTHOG_CLI_HOST=https://eu.posthog.com
+            POSTHOG_CLI_PROJECT_ID=142584
+            POSTHOG_UPLOAD_ATTEMPT=${{ github.run_id }}-${{ github.run_attempt }}
           # Keeps server action IDs the same from build to build (see the Dockerfile).
           secrets: |
             next_actions_key=${{ secrets.NEXT_SERVER_ACTIONS_BUILD_KEY }}
+            posthog_cli_api_key=${{ secrets.POSTHOG_SOURCEMAPS_API_KEY }}
           cache-from: type=gha
           cache-to: type=gha,mode=max

+ 8 - 0
.github/workflows/docker-publish.yml

@@ -72,12 +72,20 @@ jobs:
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
+          # The POSTHOG_ lines upload the browser source maps to PostHog error
+          # tracking for app.torqvoice.com when the key is set, and skip it
+          # otherwise (see scripts/upload-source-maps.sh).
           build-args: |
             APP_VERSION=${{ steps.version.outputs.value }}
             NEXT_ACTIONS_FINGERPRINT=${{ steps.actions_key.outputs.id }}
+            POSTHOG_SOURCEMAPS=${{ secrets.POSTHOG_SOURCEMAPS_API_KEY != '' && 'on' || 'off' }}
+            POSTHOG_CLI_HOST=https://eu.posthog.com
+            POSTHOG_CLI_PROJECT_ID=142584
+            POSTHOG_UPLOAD_ATTEMPT=${{ github.run_id }}-${{ github.run_attempt }}
           # Keeps server action IDs the same from release to release, so tabs
           # opened before a deploy keep working (see the Dockerfile).
           secrets: |
             next_actions_key=${{ secrets.NEXT_SERVER_ACTIONS_BUILD_KEY }}
+            posthog_cli_api_key=${{ secrets.POSTHOG_SOURCEMAPS_API_KEY }}
           cache-from: type=gha
           cache-to: type=gha,mode=max

+ 23 - 0
Dockerfile

@@ -22,6 +22,14 @@ RUN npx prisma generate
 
 ENV NEXT_TELEMETRY_DISABLED=1
 
+# Browser source maps for PostHog error tracking, so an exception on Torqvoice
+# Cloud names the file and line it came from instead of a minified chunk. The
+# image workflows set this to "on" only when they hold the upload key; any other
+# build (a fork, a self-hosted build, a plain docker build) leaves it off and
+# makes no maps. A change here is a new cache key, so a build made without maps
+# is never reused for an upload.
+ARG POSTHOG_SOURCEMAPS=off
+
 # Server action IDs are hashed from this key, the action's file path and its
 # name. Without one, Next makes a random key for every build, so each release
 # gives every action a new ID and a tab opened before the deploy fails its next
@@ -38,8 +46,23 @@ RUN --mount=type=secret,id=next_actions_key \
     if [ -s /run/secrets/next_actions_key ]; then \
       export NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="$(cat /run/secrets/next_actions_key)"; \
     fi && \
+    if [ "$POSTHOG_SOURCEMAPS" = "on" ]; then export BROWSER_SOURCE_MAPS=true; fi && \
     npm run build
 
+# Upload those maps, filed under this image's version, and delete them again,
+# since self-hosted installs run this same image. A failed upload is logged and
+# the build goes on; a map that could not be deleted fails it. See the script.
+ARG APP_VERSION=development
+ARG POSTHOG_CLI_HOST
+ARG POSTHOG_CLI_PROJECT_ID
+# The image workflows give this a new value on every run, rerun included. The
+# upload step then runs again rather than reusing a cached result, so a failed
+# upload, or one made with a key since replaced, is retried. It comes after the
+# build, so the Next build itself stays cached.
+ARG POSTHOG_UPLOAD_ATTEMPT=none
+RUN --mount=type=secret,id=posthog_cli_api_key \
+    sh scripts/upload-source-maps.sh
+
 # What the container runs beside the server: the Prisma CLI for the migrations
 # at start, and tsx with a full Prisma client for the demo seed. Installed in
 # a folder of its own, with no package.json beside it. Run inside /app, where

+ 4 - 0
next.config.ts

@@ -5,6 +5,10 @@ const withNextIntl = createNextIntlPlugin()
 
 const nextConfig: NextConfig = {
   output: 'standalone',
+  // Only for the image build that uploads them to PostHog error tracking and
+  // deletes them again (scripts/upload-source-maps.sh). Every other build,
+  // self-hosted ones included, makes none.
+  productionBrowserSourceMaps: process.env.BROWSER_SOURCE_MAPS === 'true',
   images: {
     remotePatterns: [],
   },

+ 120 - 0
scripts/upload-source-maps.sh

@@ -0,0 +1,120 @@
+#!/bin/sh
+# Sends the browser source maps of a finished `next build` to PostHog error
+# tracking, then deletes them. The Dockerfile runs it in the build stage, so an
+# exception on Torqvoice Cloud shows the file and line it came from instead of
+# a minified chunk.
+#
+# Off unless POSTHOG_SOURCEMAPS=on, which the image workflows set only when they
+# hold the upload key. Off, the build made no maps and there is nothing to do.
+#
+# An upload problem never fails the build. Without a key or a project, on a
+# platform with no pinned CLI, or when the upload goes wrong, it says so and the
+# image is built as before, only without readable stack traces.
+#
+# The maps themselves must never reach the image: self-hosted installs run the
+# same image, and anything in .next/static is served to whoever asks. If one is
+# still there at the end, the build fails.
+
+set -u
+
+STATIC_DIR=.next/static
+KEY_FILE=/run/secrets/posthog_cli_api_key
+RELEASE_NAME=torqvoice
+RELEASE_VERSION=${APP_VERSION:-development}
+PROJECT_ID=${POSTHOG_CLI_PROJECT_ID:-}
+HOST=${POSTHOG_CLI_HOST:-https://eu.posthog.com}
+
+# The CLI release to download and the SHA-256 of each archive. Bumping the
+# version means bumping both sums with it; each archive on the release page has
+# a .sha256 file beside it.
+CLI_VERSION=0.18.12
+CLI_SHA256_X86_64=4799c1e81318511885295dd22599c38aec99430559da570ff8f6c70473c257aa
+CLI_SHA256_AARCH64=1c9b8181c0bed920dfa5ce8388ac22e5418951f1a86ca95c10c4819f3feadfd8
+
+WORK_DIR=
+
+log() {
+  echo "source maps: $*"
+}
+
+has_maps() {
+  [ -n "$(find "$STATIC_DIR" -type f -name '*.map' | head -n 1)" ]
+}
+
+# Whatever happened above, no map leaves this stage. The comments pointing at
+# them go too, so a browser with dev tools open does not ask for missing files.
+finish() {
+  [ -n "$WORK_DIR" ] && rm -rf "$WORK_DIR"
+  if has_maps; then
+    find "$STATIC_DIR" -type f -name '*.map' -exec rm -f {} +
+    find "$STATIC_DIR" -type f -name '*.js' -exec sed -i '/^\/\/# sourceMappingURL=/d' {} +
+    find "$STATIC_DIR" -type f -name '*.css' -exec sed -i 's|/\*# sourceMappingURL=[^*]*\*/||' {} +
+  fi
+  if has_maps; then
+    log "could not delete the maps in $STATIC_DIR; refusing to build an image that serves them"
+    exit 1
+  fi
+  exit 0
+}
+
+if [ "${POSTHOG_SOURCEMAPS:-off}" != "on" ]; then
+  finish
+fi
+if [ ! -s "$KEY_FILE" ]; then
+  log "no upload key, skipping"
+  finish
+fi
+if [ -z "$PROJECT_ID" ]; then
+  log "no PostHog project id (POSTHOG_CLI_PROJECT_ID), skipping"
+  finish
+fi
+if ! has_maps; then
+  log "the build made no maps in $STATIC_DIR, skipping"
+  finish
+fi
+
+case "$(uname -m)" in
+  x86_64) ARCH=x86_64 SUM=$CLI_SHA256_X86_64 ;;
+  aarch64 | arm64) ARCH=aarch64 SUM=$CLI_SHA256_AARCH64 ;;
+  *)
+    log "no pinned posthog-cli for $(uname -m), skipping"
+    finish
+    ;;
+esac
+
+WORK_DIR=$(mktemp -d)
+ARCHIVE="$WORK_DIR/posthog-cli.tar.gz"
+URL="https://github.com/PostHog/posthog/releases/download/posthog-cli/v${CLI_VERSION}/posthog-cli-${ARCH}-unknown-linux-musl.tar.gz"
+
+if ! wget -q -O "$ARCHIVE" "$URL"; then
+  log "could not download posthog-cli $CLI_VERSION, skipping"
+  finish
+fi
+# A changed or tampered download is never run
+if ! echo "$SUM  $ARCHIVE" | sha256sum -c - >/dev/null 2>&1; then
+  log "the posthog-cli download does not match its pinned SHA-256, skipping"
+  finish
+fi
+if ! tar -xzf "$ARCHIVE" -C "$WORK_DIR"; then
+  log "could not unpack posthog-cli, skipping"
+  finish
+fi
+CLI="$WORK_DIR/posthog-cli-${ARCH}-unknown-linux-musl/posthog-cli"
+
+# `process` stamps each chunk with the id its map is filed under, uploads the
+# maps, and with --delete-after removes them and their comments again. The
+# stamped chunks are what the image serves, so the browser reports those ids.
+log "uploading for $RELEASE_NAME $RELEASE_VERSION to project $PROJECT_ID"
+if POSTHOG_CLI_API_KEY="$(cat "$KEY_FILE")" \
+  POSTHOG_CLI_PROJECT_ID="$PROJECT_ID" \
+  POSTHOG_CLI_HOST="$HOST" \
+  "$CLI" sourcemap process \
+  --directory "$STATIC_DIR" \
+  --release-name "$RELEASE_NAME" \
+  --release-version "$RELEASE_VERSION" \
+  --delete-after; then
+  log "uploaded"
+else
+  log "the upload failed; this image's stack traces stay minified"
+fi
+finish