JSONbored/metagraphed/.claude/skills/error-tracking-upload-source-maps-vite/SKILL.md
error-tracking-upload-source-maps-vite
Upload source maps to PostHog Error Tracking for Vite
- Source repository stars
- 12
- Declared platforms
- 0
- Static risk flags
- 3
- Last source update
- 2026-07-28
- Source checked
- 2026-07-28
Decision brief
What it does—and where it fits
This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source.
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/JSONbored/metagraphed --skill ".claude/skills/error-tracking-upload-source-maps-vite"Inspect the Agent Skill "error-tracking-upload-source-maps-vite" from https://github.com/JSONbored/metagraphed/blob/bd49f179f9b4597bd8467ab32783f27108e0bb28/.claude/skills/error-tracking-upload-source-maps-vite/SKILL.md at commit bd49f179f9b4597bd8467ab32783f27108e0bb28. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Test the local setup
Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards.
The handler must call the SDK's exception-capture method directly — do not throw. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms.Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved.Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI. - 02
Reference files
The overview lists every supported framework and build tool. The CLI reference covers posthog-cli sourcemap process, which injects chunk IDs and uploads maps in one step.
references/vite.md - Upload source maps for vite - docsreferences/upload-source-maps.md - Upload source maps - docsreferences/cli.md - Upload source maps with cli - docs - 03
Steps
The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under Tips, and per-technology notes under Examples. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow…
The public project key (the one in your SDK init) will not work for uploads — it has no write scope for symbol sets.Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file").Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. - 04
Get a personal API key
Source map upload authenticates with a personal API key, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page.
The public project key (the one in your SDK init) will not work for uploads — it has no write scope for symbol sets.Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file").Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret. - 05
Tips
The public project key (the one in your SDK init) will not work for uploads — it has no write scope for symbol sets.
The public project key (the one in your SDK init) will not work for uploads — it has no write scope for symbol sets.Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file").Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret.
Permission review
Static risk signals and limitations
Writes files
The documentation asks the agent to create, modify, or delete local files.
Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file").Runs scripts
The documentation asks the agent to run terminal commands or scripts.
**Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Then run `posthog-cli sourcemap process` against the build output dir as a post-build step — it injects chunRuns scripts
The documentation asks the agent to run terminal commands or scripts.
A Run Script phase, ordered last, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` in its Input Files, calling the SDK's bundled script — do not hand-roll the upload:Writes files
The documentation asks the agent to create, modify, or delete local files.
### Write credentials to the env fileReads files
The documentation asks the agent to read local files, directories, or repositories.
Build with `docker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .`. In `docker/build-push-action`, pass the key through the `secrets:` input (`POSTHOG_CLI_Reads files
The documentation asks the agent to read local files, directories, or repositories.
Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind.Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 85/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 12 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- JSONbored/metagraphed
- Skill path
- .claude/skills/error-tracking-upload-source-maps-vite/SKILL.md
- Commit
- bd49f179f9b4597bd8467ab32783f27108e0bb28
- License
- AGPL-3.0
- Collected
- 2026-07-28
- Default branch
- main
View the original SKILL.md
Upload source maps to PostHog for Vite
This skill helps you upload source maps (or platform debug symbols) so PostHog Error Tracking can resolve minified stack traces back to your original source.
Reference files
references/vite.md- Upload source maps for vite - docsreferences/upload-source-maps.md- Upload source maps - docsreferences/cli.md- Upload source maps with cli - docsreferences/COMMANDMENTS.md- Framework-specific rules the integration must follow
The overview lists every supported framework and build tool. The CLI reference covers posthog-cli sourcemap process, which injects chunk IDs and uploads maps in one step.
Steps
The stages of wiring up source map upload, in order. Each step has a short overview, gotchas under Tips, and per-technology notes under Examples. The reference files above are the source of truth for the exact, per-framework API — when this page and a reference disagree, follow the reference for Vite.
Get a personal API key
Source map upload authenticates with a personal API key, not the public project API key the SDK uses at runtime. The key needs error-tracking write access; the quickest path is the "Source map upload" preset on PostHog's personal API keys settings page.
Tips
- The public project key (the one in your SDK
init) will not work for uploads — it has no write scope for symbol sets. - Never hardcode the key in source. It belongs in an environment variable read at build time (see "Write credentials to the env file").
- Keys can't be minted programmatically — create them by hand in PostHog settings, then store the value as a secret.
Apply build-config changes
Wire source map generation, chunk-ID injection, and upload into your production build so every deploy ships matching maps. Depending on the platform this is either a build/bundler plugin, or a posthog-cli sourcemap process step run after the build (it injects chunk IDs and uploads in one pass). Follow the Vite reference for the exact wiring.
Tips
- If you wire
posthog-clidirectly (no framework or bundler plugin), generating the maps is your responsibility — the CLI only injects chunk IDs into, and uploads, maps your build already produced. Two things must be true beforeposthog-cli sourcemap processworks:- Source maps are emitted next to your output bundles (e.g.
.js.mapfiles). - The maps include
sourcesContent(the original source embedded inside the map). Without it PostHog has the line/column mappings but not the code, so traces can't be fully resolved.
- Source maps are emitted next to your output bundles (e.g.
- Inject before deploy: the injected bundles must be the ones shipped to production. Bundles missing the
//# chunkId=…comment can't be matched to uploaded maps. - Wire injection + upload into the build itself (plugin, post-build script, or CI step) — manual uploads drift from deployed code.
- Don't ship source maps publicly: omit
.mapfiles from the deployed artifact, or use hidden source maps. Uploaded maps live in PostHog, not on your origin. - Link each release to its commit. The CLI auto-detects the commit from the CI's git env vars — see "Associate the release with a git commit" for making those reachable in Docker/CI builds.
Examples
- Node / tsc Emit maps with embedded sources by setting both in
tsconfig.json:"sourceMap": trueand"inlineSources": true. Then runposthog-cli sourcemap processagainst the build output dir as a post-build step — it injects chunk IDs and uploads in one pass, and needs the upload credentials (see "Make credentials available at build time"). - Vite / Webpack / Rollup Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. Make sure the bundler is configured to emit source maps.
- iOS (Xcode) iOS uploads dSYM debug symbols, not source maps. Required target changes:
DEBUG_INFORMATION_FORMAT = dwarf-with-dsymfor Release.ENABLE_USER_SCRIPT_SANDBOXING = NO.- A Run Script phase, ordered last, with
$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)in its Input Files, calling the SDK's bundled script — do not hand-roll the upload:- SPM:
POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" - CocoaPods:
POSTHOG_INCLUDE_SOURCE=1 POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"Copy the invocation verbatim — thePOSTHOG_INCLUDE_SOURCE=1andPOSTHOG_CLI_DOTENV_FILEprefixes HAVE to be there. This needs a recentposthog-cli(older ones silently ignorePOSTHOG_CLI_DOTENV_FILE); the PostHog wizard installs it for you, so do not runnpm install -gyourself.
- SPM:
- Android (Gradle) Android uploads ProGuard/R8 mapping files, not source maps. Apply the
com.posthog.androidGradle plugin on the app module'sbuild.gradle(.kts)(never the root project), per the reference — the plugin hooks the build and uploads automatically, do not hand-roll aposthog-clistep. Gotchas:- The plugin only hooks minified variants — if the release build type has
isMinifyEnabled = false, set it totrue(keep the existingproguardFilesline) or nothing is uploaded. - The upload shells out to
posthog-clion thePATH(v0.7.4+); the PostHog wizard installs it for you, so do not runnpm install -gyourself. - The Gradle plugin is versioned separately from the
posthog-androidSDK — never reuse the SDK version inid("com.posthog.android") version "…".
- The plugin only hooks minified variants — if the release build type has
- Next.js / Nuxt / Angular Use the framework's documented source-map upload integration from the reference; these own their build pipeline, so configure upload there rather than bolting on a separate CLI step.
- React Native / Flutter You upload platform debug symbols (Hermes maps, dSYMs) rather than plain
.js.mapfiles — follow the platform reference for the exact build hook.
Make credentials available at build time
The upload credentials must be readable by the build pipeline at build time, not merely present in a .env file. Whether .env is auto-loaded depends on the technology.
Tips
- Auto-loads
.env: Next.js, Nuxt and similar frameworks read.envinto the build for you — nothing extra to do. - Vite is a partial exception: it auto-loads
.envintoimport.meta.envfor client code (onlyVITE_-prefixed vars), but does not put vars inprocess.envfor your config to read. The upload credentials (POSTHOG_*, notVITE_-prefixed) are read when the plugin is constructed, so load them yourself — see the Vite example below. - Does NOT auto-load
.env: Rollup, plain webpack, and plain Node scripts. Load it explicitly — adddotenv(require('dotenv').config(), orimport 'dotenv/config'for ESM) at the top of the bundler/config file. - Separate-process gotcha: if
posthog-cli sourcemap processruns as its ownpackage.jsonstep (after the bundler), the CLI call is a separate child process and will not see env vars a loader set inside the bundler config. Point the CLI at the file directly:posthog-cli --dotenv-file <relative-path> sourcemap process …(the flag goes before the subcommand). processauthenticates from the start.posthog-cli sourcemap processresolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass--dotenv-fileto theprocessinvocation. (It can still appear to work if the developer once ranposthog-cli login, which leaves credentials in~/.posthog— that won't exist in CI or on a teammate's machine.)- iOS / Xcode No loader — the Run Script phase's
POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"prefix points posthog-cli at the gitignored.env.POSTHOG_CLI_HOSTis the API host (https://us.posthog.com), never the*.i.posthog.comingestion host. - Android / Gradle Gradle does not read
.env— bridge it in the app module's build script (see the Android example). Unset properties fall back to realPOSTHOG_CLI_*environment variables, so the same wiring works in CI. The host var follows the same API-host rule as iOS above.
Examples
- Next.js / Nuxt Auto-load
.envat build time; put the vars there and you're done. - Vite Export
vite.configas a function and mergeloadEnvintoprocess.envso the config (and the PostHog plugin) can read the upload credentials. Pass''as the third arg so non-VITE_vars likePOSTHOG_API_KEYare included — the default'VITE_'prefix skips them:import { defineConfig, loadEnv } from "vite"; export default ({ mode }) => { process.env = { ...process.env, ...loadEnv(mode, process.cwd(), "") }; // process.env.POSTHOG_API_KEY is now readable by the plugins below return defineConfig({ plugins: [/* … posthog source map plugin … */], }); }; - Rollup / webpack / plain Node Add
import 'dotenv/config'(orrequire('dotenv').config()) at the top of the config/entry file so the loader runs before the build reads the vars. - Standalone posthog-cli step Pass
--dotenv-file .envto theprocessinvocation so it can authenticate:"build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" - iOS (Xcode / posthog-cli) A gitignored
.envnext to the.xcodeproj— the Run Script invocation'sPOSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set thePOSTHOG_CLI_*values as job secrets instead — no.envon the runner. - Android (Gradle / posthog-cli) A gitignored
.envat the Gradle project root, bridged into the upload tasks in the app module'sbuild.gradle.kts:
(Groovyimport com.posthog.android.PostHogCliExecTask import java.util.Properties val postHogEnv = Properties().apply { val envFile = rootProject.file(".env") if (envFile.exists()) envFile.inputStream().use { load(it) } } tasks.withType<PostHogCliExecTask>().configureEach { postHogEnv.getProperty("POSTHOG_CLI_API_KEY")?.let { postHogApiKey.set(it) } postHogEnv.getProperty("POSTHOG_CLI_PROJECT_ID")?.let { postHogProjectId.set(it) } postHogEnv.getProperty("POSTHOG_CLI_HOST")?.let { postHogHost.set(it) } }build.gradle: same shape withtasks.withType(PostHogCliExecTask).configureEach { … }.) In CI, set thePOSTHOG_CLI_*values as job secrets instead — no.envon the runner.
Write credentials to the env file
Write the personal API key and project identifiers into the env file your build reads. Reuse the file the project already uses — don't introduce a second one.
Tips
- Picking the file: if an env file already contains PostHog vars (
POSTHOG_*/NEXT_PUBLIC_POSTHOG_*), use that one. Otherwise, if exactly one env file exists use it; if several exist prefer.env. Only create a new file when none exists. - Variable names depend on which uploader you wired:
posthog-clidirect upload →POSTHOG_CLI_API_KEY,POSTHOG_CLI_PROJECT_ID,POSTHOG_CLI_HOST- bundler-plugin variants →
POSTHOG_API_KEY,POSTHOG_PROJECT_ID,POSTHOG_HOST
- Set the
*_HOSTvar when you're not on US Cloud's default (e.g. EU Cloud or self-hosted); setting it explicitly always is safe. Follow the reference for the variant. - In CI/CD, set the same vars as secrets — never commit the key.
Identify the build and run commands
Resolve two concrete commands for this project: the production build command (the one that uploads source maps) and the run command that launches the built app (so a test error can be triggered against the real artifact).
Tips
- Resolve real commands from the project's actual scripts/config — substitute the correct package manager. Never leave a generic "start the app".
- When a build artifact is involved, prefer the command that serves the production build over the dev server.
Examples
- Next.js Build:
npm run build(next build). Run:npm run start(next start). - Vite Build:
npm run build. Run:npm run preview. - Plain Node Build:
npm run build. Run:node <built entry>— read package.jsonmain/binand the build output dir to name the real file (e.g.node dist/index.js). - Android Build:
./gradlew assembleRelease. Run: launch on a device/emulator (Android Studio, or./gradlew installRelease). - iOS Local build + run are one step: Xcode Run with Build Configuration = Release.
xcodebuildis CI-only. - Flutter Build:
flutter build apk/flutter build ios. Run:flutter run. - React Native Run:
npx react-native run-ios/npx react-native run-android.
Set up CI for automatic uploads
Source maps are only uploaded when the production build runs, so the environment that builds and deploys your app needs the same upload credentials you put in the env file. The whole job is: find where the production build command actually runs, then make the upload credentials reachable at that exact spot. Only ever edit CI/deploy files that already exist — never create a new workflow, pipeline, or deploy file. Wiring credentials means modifying the build/deploy config this project already has; it is never license to author new CI. The build is where maps inject + upload, and env does not automatically cross three boundaries — into a Docker build, into a nested/composite action, or into an SSH session. So trace the deploy path before editing anything:
- Is there a
Dockerfile? If the build command runs inside it (RUN <build>), the build happens in that image's build stage. - Is there a workflow under
.github/workflows/? Open it and find the step that triggers the build, then follow it to where the build truly executes — it may be:- an inline build step (
run: npm run build) on the runner, - a
docker build/docker/build-push-actionstep (build runs in the image), - a
uses: ./.github/actions/...local composite action — open thataction.yml; the real build step is one layer down, - an
ssh/deploy step (e.g.appleboy/ssh-action) whosescript:runs the build on a remote server.
- an inline build step (
- Any other CI config in the repo (
.gitlab-ci.yml,.circleci/config.yml,Jenkinsfile,bitbucket-pipelines.yml,azure-pipelines.yml, …)? Open it and find the job/stage that runs the production build. The principle is identical; apply it with your working knowledge of that provider — the examples below show the pattern to mirror. - No
Dockerfile, no CI config, no build step you can trace? Don't guess — tell the user where the creds need to be (see "Untraceable setup" under Examples).
Tips
- A deploy file for another package is not license to author one for this one. In a monorepo especially, finding a workflow that deploys a sibling package (e.g. a
deploy-backend.yml, or aDockerfile/pipeline for another app) does not mean you should create a matchingdeploy-frontend.yml(or any new CI file) for the project you're instrumenting. Wire credentials only into the existing file that builds this project. If this project has no build/deploy config you can open and edit, it is untraceable: make no CI changes and hand the requirement to the user (see "Untraceable setup") — do not invent one. - Reuse the exact variable names from "Write credentials to the env file" — the build reads the same names locally and in CI. (
POSTHOG_CLI_*for directposthog-cli;POSTHOG_*for bundler-plugin uploaders.) - In CI, credentials travel as environment variables — never as a file. Do not materialize a
.envon the runner (e.g.printf … > .envbefore the build), and never copy or un-ignore one into a Docker image: it's redundant, and a secrets file on disk can leak into artifacts, caches, or image layers. A build script that passes--dotenv-file .envtoposthog-cliworks unchanged in CI even though.envdoesn't exist there: real environment variables take precedence over the file, and a missing file is skipped with a warning. - Never commit secret values. Reference credentials by name only: Docker
ARG/ENVor BuildKit secret ids,${{ secrets.* }}in GitHub Actions. The personal API key stays out of version control. - Layers stack — a workflow can call a composite action that runs
docker buildagainst a Dockerfile. Wire every layer the credentials must pass through, from the outer${{ secrets.* }}reference down to theARG/ENVin the build stage. - Multi-stage Dockerfiles: put the
ARG/ENVin the build stage (where the build command runs), never the runtime stage. That's both correct (the build needs them) and safer (the creds don't get baked into the shipped image). - Single-stage Dockerfiles: with no separate build stage,
ARG/ENVwould bake the API key into the shipped image (docker inspectrevealsENV;docker historycan reveal build args). Mount the key as a BuildKit secret on the buildRUNinstead — it exists for that command only and is never written to a layer (see the single-stage example). PlainARG/ENVstays fine for the non-secret project ID and host. - Composite / reusable actions can't read
secrets. Inside a.github/actions/*/action.ymlonly${{ inputs.* }}is available. Add aninputs:entry per credential, reference${{ inputs.* }}there, and pass${{ secrets.* }}from the calling workflow'swith:block. - Build over SSH: the runner's env doesn't reach the remote box. Set the vars inline immediately before the build command inside the
script:.${{ secrets.* }}is substituted by Actions before the script is sent, so the value travels with the script. - The worked examples are exemplars, not an allowlist. For any provider not shown (GitLab CI, CircleCI, Jenkins, Bitbucket, Azure Pipelines, …), apply the same principle with your knowledge of that provider: find the job that runs the production build, expose the credentials there via the provider's native secret mechanism (GitLab project CI/CD variables, CircleCI project env vars / contexts, Jenkins credentials +
withCredentials, …), and cross the same boundaries the same way — Docker builds still need--build-arg, SSH sessions still need inline vars. - Make only the edits the provider actually needs. Some providers inject project-level variables straight into every job's environment — GitLab CI/CD variables work this way — so an inline build step may need no functional pipeline change at all. When that's the conclusion, still add a short comment on the build job naming the required variables and where to create them (see the GitLab example) — the requirement must be visible in the repo, not only in your hand-off — and tell the user exactly which variables to create and where.
- You can't create CI secrets. Whenever the pipeline reads a credential, tell the user where to add it before their next deploy — GitHub: Settings → Secrets and variables → Actions; GitLab: Settings → CI/CD → Variables; other providers: their equivalent secret store. The pipeline can't read a secret that doesn't exist yet.
Examples
- Dockerfile build stage (e.g.
Dockerfile, no CI) Declare the credentials as build args and promote them to env vars before the buildRUN, in the build stage:
With no CI wiring the image, tell the user to pass them when they build:FROM node:22-slim AS build WORKDIR /app # ... ARG POSTHOG_CLI_API_KEY ARG POSTHOG_CLI_PROJECT_ID ARG POSTHOG_CLI_HOST ENV POSTHOG_CLI_API_KEY=$POSTHOG_CLI_API_KEY \ POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST RUN npm run build # now sees the upload credentialsdocker build --build-arg POSTHOG_CLI_API_KEY=… --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… . - Single-stage Dockerfile (BuildKit secret) When build and runtime share one stage, pass the API key as a BuildKit secret so it never lands in the image; keep
ARG/ENVfor the non-secret project ID and host:
Build with# syntax=docker/dockerfile:1 FROM node:22-slim WORKDIR /app # ... ARG POSTHOG_CLI_PROJECT_ID ARG POSTHOG_CLI_HOST ENV POSTHOG_CLI_PROJECT_ID=$POSTHOG_CLI_PROJECT_ID \ POSTHOG_CLI_HOST=$POSTHOG_CLI_HOST RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY \ npm run builddocker build --secret id=POSTHOG_CLI_API_KEY,env=POSTHOG_CLI_API_KEY --build-arg POSTHOG_CLI_PROJECT_ID=… --build-arg POSTHOG_CLI_HOST=… .. Indocker/build-push-action, pass the key through thesecrets:input (POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }}) instead ofbuild-args:. Theenv=attribute on--mountneeds a current BuildKit — keep the# syntax=docker/dockerfile:1line; on engines too old for it, read the file form instead:RUN --mount=type=secret,id=POSTHOG_CLI_API_KEY POSTHOG_CLI_API_KEY=$(cat /run/secrets/POSTHOG_CLI_API_KEY) npm run build. - GitHub Actions — inline build step Build runs on the runner; expose the creds with
env:on that step:- name: Build run: npm run build env: POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }} POSTHOG_CLI_PROJECT_ID: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} POSTHOG_CLI_HOST: ${{ secrets.POSTHOG_CLI_HOST }} - GitHub Actions —
docker build/docker/build-push-actionAdd theARG/ENVto the Dockerfile build stage (above), then forward the creds as build args. Rawdocker buildtakes--build-arg;docker/build-push-actiontakes a multi-linebuild-args:input — merge into the existingwith:block, don't add a second step:- name: Build and push image uses: docker/build-push-action@v6 with: context: . file: Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} build-args: | POSTHOG_CLI_API_KEY=${{ secrets.POSTHOG_CLI_API_KEY }} POSTHOG_CLI_PROJECT_ID=${{ secrets.POSTHOG_CLI_PROJECT_ID }} POSTHOG_CLI_HOST=${{ secrets.POSTHOG_CLI_HOST }} - GitHub Actions — nested/composite action When the workflow delegates the build with
uses: ./.github/actions/build-and-push, thebuild-push-actionlives in that action'saction.yml, which can't seesecrets. Thread them through as inputs. In.github/actions/build-and-push/action.yml:
Then pass the secrets from the calling workflow'sinputs: posthog-cli-api-key: required: true posthog-cli-project-id: required: true posthog-cli-host: required: true runs: using: composite steps: - uses: docker/build-push-action@v6 with: # ...existing context/file/push/tags... build-args: | POSTHOG_CLI_API_KEY=${{ inputs.posthog-cli-api-key }} POSTHOG_CLI_PROJECT_ID=${{ inputs.posthog-cli-project-id }} POSTHOG_CLI_HOST=${{ inputs.posthog-cli-host }}with:block:- uses: ./.github/actions/build-and-push with: # ...existing inputs... posthog-cli-api-key: ${{ secrets.POSTHOG_CLI_API_KEY }} posthog-cli-project-id: ${{ secrets.POSTHOG_CLI_PROJECT_ID }} posthog-cli-host: ${{ secrets.POSTHOG_CLI_HOST }} - GitHub Actions — build over SSH When a step SSHes into a server and runs the build there (e.g.
appleboy/ssh-actionwithgit pull && npm run build), set the vars inline right before the build command inside thescript:— mirror however the script already passes runtime vars:- uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEPLOY_HOST }} # ... script: | cd /srv/app && git pull --ff-only origin main && npm ci POSTHOG_CLI_API_KEY="${{ secrets.POSTHOG_CLI_API_KEY }}" \ POSTHOG_CLI_PROJECT_ID="${{ secrets.POSTHOG_CLI_PROJECT_ID }}" \ POSTHOG_CLI_HOST="${{ secrets.POSTHOG_CLI_HOST }}" \ npm run build - GitLab CI (
.gitlab-ci.yml) Project CI/CD variables are injected into every job's environment automatically, so a job that runs the build inline (script: - npm run build) needs no functional YAML change — novariables:block, and do NOT add a script line that writes the variables into a.envfile (printf … > .env,echo … >> .env, etc.); the build already sees them as environment variables, which take precedence over any dotenv file. DO leave a comment on the build job so the requirement is visible in the repo, not only in your hand-off:
Then tell the user to add those variables in Settings → CI/CD → Variables and the next pipeline picks them up. Edits beyond the comment are only needed when a boundary is crossed: a job that runsbuild: stage: build # PostHog source map upload: this job needs POSTHOG_CLI_API_KEY, # POSTHOG_CLI_PROJECT_ID and POSTHOG_CLI_HOST available as CI/CD # variables (Settings → CI/CD → Variables); GitLab injects them into # the job automatically. Mark them Masked — but Protected only if this # job runs exclusively on protected branches, otherwise feature-branch # builds fail with missing credentials. script: - npm ci - npm run builddocker buildmust forward them (--build-arg POSTHOG_CLI_API_KEY="$POSTHOG_CLI_API_KEY" …) into the Dockerfile's build stage (see the Dockerfile example), and a job that builds over SSH must set them inline before the remote build command, exactly like the SSH example above. - Other CI providers (CircleCI, Jenkins, Bitbucket, Azure Pipelines, …) Same recipe, provider-native mechanics: open the pipeline config, find the job that runs the production build, expose the credentials to that job via the provider's secret store, and thread them through any Docker/SSH boundary just like the examples above. Reference credentials by name only, then tell the user each secret to create and exactly where in the provider's UI it goes.
- Untraceable setup No
Dockerfile, no CI config, and no build step you can trace: make no CI changes — do not author a new workflow, pipeline, or deploy file to fill the gap. Tell the user that wherever their production build command runs, it must have the upload credentials (POSTHOG_CLI_*/POSTHOG_*) available as environment variables, or maps won't upload on deploy. If part of the path is still recognisable — e.g. aDockerfilebuilt by an unfamiliar CI — wire the layers you do recognise and tell the user exactly what the remaining layer must pass in (e.g. the--build-argflags).
Associate the release with a git commit
posthog-cli links the release to a git commit, branch and repo so Error Tracking can show which deploy an error came from. It auto-detects that from the CI's git env vars or a local .git directory — you never touch the CLI invocation itself (it's usually baked into npm run build or a bundler plugin), you just make the git context available in the build environment. A docker build is where this breaks: it sees neither the env vars nor .git (the same boundary credentials hit), so the release ends up linked to nothing unless you forward the vars in.
Tips
- Forward GitHub's git env vars into the Docker build the same way you forwarded credentials. Declare each as an
ARGand promote it toENV—ARGalone isn't visible to the CLI's env lookup. That's all auto-detection needs; no CLI flags, no.git.
Examples
- GitHub Actions → docker build Forward GitHub's git vars into the build stage and the CLI auto-detects branch + repo + commit:
Then in the build stage, declare each asbuild-args: | GITHUB_ACTIONS=true GITHUB_SHA=${{ github.sha }} GITHUB_REF_NAME=${{ github.ref_name }} GITHUB_REPOSITORY=${{ github.repository }} GITHUB_SERVER_URL=${{ github.server_url }}ARGand re-export it asENVbefore the build runs. - Inline CI build (no Docker) GitHub Actions already sets these vars on the runner, so auto-detection just works — nothing to pass.
Test the local setup
Optionally add a temporary, clearly-labeled affordance that captures one test exception, so you can confirm errors arrive in Error Tracking with a source-resolved stack trace after the next production build. Always remove it afterwards.
Tips
- The handler must call the SDK's exception-capture method directly — do not
throw. Throwing depends on the global error handler and shows a dev overlay; a direct capture is deterministic across platforms. - Pass a single Error (or platform-equivalent throwable). No custom message beyond the Error, no extra properties, no second argument — the Error's stack trace is what gets resolved.
- Use distinctive copy on the trigger (button label / route path) so the resulting event is easy to find in the UI.
- Read any file before editing it and capture its exact contents; after testing, restore every file the affordance touched — the affordance only, leave the upload and credential wiring in place — and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first.
- The upload only happens on the production build: build, run, trigger the error, then confirm the stack trace in Error Tracking points at real source files, not minified bundle paths.
Examples
- Browser / SPA / SSR (web, react, nextjs, nuxt, angular, vite, webpack, rollup) Add a button such as "Test PostHog Error Tracking" on the home/root page whose onClick calls
posthog.captureException(new Error("PostHog source maps test")). - Node.js Add a temporary route (e.g.
GET /__posthog-test-error) on the existing server that callsposthog.captureException(new Error("PostHog source maps test"))and returns 200. With no HTTP layer, add the capture to the existing entry script where the client is initialised rather than creating a new file. Tell the user the exact command/URL to hit. - React Native Add a visible
Buttonon the main screen whose onPress callsposthog.captureException(new Error("PostHog source maps test")). - Android (Kotlin) Add a
Buttonon the launcher Activity whose onClick handler is exactly:
Test flow — the upload only runs on the minified release variant:import com.posthog.PostHog PostHog.captureException(Throwable("PostHog source maps test"))./gradlew installRelease(or Android Studio ▸ Build Variants ▸ release, then Run), launch the app, tap the button. It's an event, not a crash — the app keeps running. - iOS (Swift)
Buttonon the root view (SwiftUI) orUIButtonon the root view controller (UIKit), handler:
(do { throw NSError(domain: "PostHogSourceMapTest", code: 1, userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) } catch { PostHogSDK.shared.captureException(error) }capture()takes an event-name String, not an Error.) Test flow — give the user these steps verbatim, everything happens in Xcode (noxcodebuild): 1) In Xcode: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release, then Run — the Release build uploads dSYMs automatically. 2) Tap the "" button in the app. It's an event, not a crash — no debugger-detach or relaunch steps. - Flutter Add an
ElevatedButtonon the home widget whose onPressed callsPosthog().captureException(Exception("PostHog source maps test")).
Verify and hand off
Confirm the upload landed and report what changed.
Tips
- Source maps upload during the production build — the build must actually run for a symbol set to appear.
- Verify in PostHog Error Tracking settings on the Symbol sets page: a new symbol set should appear after the build completes.
- When handing off, list the files you edited (paths only), the env-var key names you set (never values), whether a test affordance was added and reverted, and the exact build command to run.
- If you wired CI, list the pipeline files you changed (
Dockerfile, workflow, pipeline config) and spell out every manual follow-up — e.g. the secrets the user must add in their CI provider's settings before their next deploy, or the note that their build path couldn't be traced.
General tips
- The reference files for Vite are authoritative — if this page and a reference disagree on an API, follow the reference.
- Two different keys, two different jobs: a personal API key uploads maps at build time; the public project key powers the SDK at runtime. Don't swap them.
- Keep build artifacts and uploaded maps in sync — every deploy should inject + upload within the same build so stack traces always resolve.
- Uploaded maps live in PostHog and never need to be served publicly.
- Detect the project's package manager before installing any dependency.
- Read a file (and note its exact contents) immediately before editing it — essential for any temporary test code you'll revert afterwards.
Framework guidelines
- A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message " variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once is configured" (substituting the actual variable name); production stays a no-op
- Remember that source code is available in the node_modules directory
- Check package.json for type checking or build scripts to validate changes
- When identity comes from framework-bridged state (Inertia or SSR shared props, a serialized session), confirm the backend actually shares that field — add the share server-side if missing — before identifying from it
Alternatives
Compare before choosing
event4u-app/agent-config
laravel-horizon
Use when working with Laravel queues in production — Horizon dashboard, worker supervision, job metrics, balancing strategies — even when the user just says 'my jobs are piling up'.
affaan-m/ECC
mle-workflow
Production machine-learning engineering workflow for data contracts, reproducible training, model evaluation, deployment, monitoring, and rollback. Use when building, reviewing, or hardening ML systems beyond one-off notebooks.
K-Dense-AI/scientific-agent-skills
simpy
Build, inspect, test, and analyze bounded process-based discrete-event simulations with SimPy, including events, resources, interrupts, monitoring, replications, warm-up, and reproducible output analysis.
JasonColapietro/suede-creator-skills
suede-code-grader
Give a blunt A-F ship grade for a code change across correctness, security, data, UX, verification, and deploy readiness. Use for a grade, not a findings review.