From 54a4353e526ffd0af74e38a2e83293c8c0e33982 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Sat, 11 Jul 2026 09:21:46 +0200 Subject: [PATCH 01/16] test: lock iOS source-map skill contract --- .../error-tracking-upload-source-maps.test.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/lib/tests/error-tracking-upload-source-maps.test.js diff --git a/scripts/lib/tests/error-tracking-upload-source-maps.test.js b/scripts/lib/tests/error-tracking-upload-source-maps.test.js new file mode 100644 index 00000000..987afd96 --- /dev/null +++ b/scripts/lib/tests/error-tracking-upload-source-maps.test.js @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { join } from 'path'; + +import { expandSkillGroups, loadSkillsConfig } from '../skill-generator.js'; + +const CONFIG_DIR = join(process.cwd(), 'context'); + +describe('error-tracking-upload-source-maps iOS variant', () => { + it('expands to the skill contract consumed by the wizard', () => { + const config = loadSkillsConfig(CONFIG_DIR); + const skills = expandSkillGroups(config, CONFIG_DIR); + const ios = skills.find((skill) => skill.id === 'error-tracking-upload-source-maps-ios'); + + expect(ios).toMatchObject({ + id: 'error-tracking-upload-source-maps-ios', + _shortId: 'ios', + _category: 'error-tracking-upload-source-maps', + _group: 'error-tracking-upload-source-maps', + display_name: 'iOS', + description: 'Upload dSYM debug symbols to PostHog Error Tracking for iOS', + tags: ['error-tracking', 'source-maps', 'ios', 'swift'], + docs_urls: ['https://posthog.com/docs/error-tracking/upload-source-maps/ios.md'], + _sharedDocs: [ + 'https://posthog.com/docs/error-tracking/upload-source-maps.md', + 'https://posthog.com/docs/error-tracking/upload-source-maps/cli.md', + ], + _cli: null, + }); + }); +}); From f5e0a839807590b10345a4730ecc28ace28e22e5 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Tue, 14 Jul 2026 18:27:36 +0200 Subject: [PATCH 02/16] feat: document iOS xcconfig credential flow for source-map upload iOS has no .env at build time, so document how the dSYM-upload credentials reach the Xcode build. Add an iOS branch to "Make credentials available at build time": the secret personal API key goes in a gitignored xcconfig surfaced to the Run Script phase as a build setting, while the non-secret project id / host are exported in that phase (kept out of xcconfig, where `//` starts a comment and would truncate the host URL). Lock the contract with a test on the rendered skill body. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../description.md | 15 +++++++++++ .../error-tracking-upload-source-maps.test.js | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 310e615e..48d14e24 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -50,6 +50,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) +- **iOS / Xcode does NOT use `.env`.** The dSYM upload runs as an Xcode **Run Script build phase** and Xcode never loads a `.env`. Put the secret personal API key in a **gitignored `.xcconfig`** assigned to the target's build configuration — Xcode exports every build setting into the Run Script phase's environment, so the script sees `$POSTHOG_CLI_API_KEY`. Keep the non-secret `POSTHOG_CLI_PROJECT_ID` / `POSTHOG_CLI_HOST` as `export` lines in the Run Script phase itself, **not** in the xcconfig — in xcconfig `//` begins a comment and would truncate `https://…`. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -70,6 +71,19 @@ The upload credentials must be readable **by the build pipeline at build time**, ```json "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" ``` +- **iOS (Xcode / posthog-cli)** The posthog-ios SDK ships `upload-symbols.sh`, run as a **Run Script build phase**; it calls `posthog-cli`, which reads `POSTHOG_CLI_*` from the environment. There is no `.env` — wire the credentials through Xcode instead: + 1. Create a **gitignored** `PostHog.xcconfig` holding only the secret (commit a `PostHog.example.xcconfig` with an empty value so teammates see the setting exists): + ``` + POSTHOG_CLI_API_KEY = phx_your_personal_api_key + ``` + 2. Assign `PostHog.xcconfig` to the target's build configuration (Project ▸ Info ▸ Configurations, or `baseConfigurationReference` in the pbxproj). Xcode then exposes `POSTHOG_CLI_API_KEY` to Run Script phases as an environment variable. + 3. In the dSYM-upload Run Script phase, export the two non-secret values before invoking the script: + ```bash + export POSTHOG_CLI_PROJECT_ID=12345 + export POSTHOG_CLI_HOST=https://us.posthog.com + "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" + ``` + In CI, do **not** ship the xcconfig — set all three `POSTHOG_CLI_*` as job/environment secrets (Xcode Cloud environment variables, Fastlane `ENV`, GitHub Actions `env:`). Real environment variables take precedence and the build phase inherits them. ### Write credentials to the env file @@ -77,6 +91,7 @@ Write the personal API key and project identifiers into the env file your build #### 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. +- **iOS exception**: there is no env file. Write only the secret `POSTHOG_CLI_API_KEY` to a gitignored `.xcconfig` (see "Make credentials available at build time"); the project id and host are exported in the Run Script phase, not written to a file. - Variable names depend on which uploader you wired: - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` diff --git a/scripts/lib/tests/error-tracking-upload-source-maps.test.js b/scripts/lib/tests/error-tracking-upload-source-maps.test.js index 987afd96..f184874d 100644 --- a/scripts/lib/tests/error-tracking-upload-source-maps.test.js +++ b/scripts/lib/tests/error-tracking-upload-source-maps.test.js @@ -1,9 +1,19 @@ import { describe, expect, it } from 'vitest'; import { join } from 'path'; +import { readFileSync } from 'fs'; import { expandSkillGroups, loadSkillsConfig } from '../skill-generator.js'; const CONFIG_DIR = join(process.cwd(), 'context'); +const SKILL_BODY = readFileSync( + join( + CONFIG_DIR, + 'skills', + 'error-tracking-upload-source-maps', + 'description.md', + ), + 'utf8', +); describe('error-tracking-upload-source-maps iOS variant', () => { it('expands to the skill contract consumed by the wizard', () => { @@ -27,4 +37,20 @@ describe('error-tracking-upload-source-maps iOS variant', () => { _cli: null, }); }); + + it('routes iOS build-time credentials through a gitignored xcconfig, not .env', () => { + // iOS never loads a .env at build time — the secret must land in an + // xcconfig surfaced to the Run Script phase, and the non-secret values + // stay out of that file (host contains `//`, which comments in xcconfig). + expect(SKILL_BODY).toMatch(/iOS \/ Xcode does NOT use `\.env`/); + expect(SKILL_BODY).toMatch(/gitignored/); + expect(SKILL_BODY).toContain('PostHog.xcconfig'); + expect(SKILL_BODY).toContain('POSTHOG_CLI_API_KEY = phx_'); + expect(SKILL_BODY).toMatch( + /export POSTHOG_CLI_PROJECT_ID[\s\S]*export POSTHOG_CLI_HOST/, + ); + // The secret write step must flag the iOS exception so the agent does + // not fall back to the dotenv flow. + expect(SKILL_BODY).toMatch(/\*\*iOS exception\*\*/); + }); }); From 88a8b12f7b52dd868a5f4e150ba7438db3cf2e32 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 12:30:01 +0200 Subject: [PATCH 03/16] feat: pin the verified iOS dSYM upload pipeline in the source-maps skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A live iOS run hallucinated five things the skill left unspecified. Pin each to its verified form so agents cannot reinvent them: - Upload via the SDK-bundled upload-symbols.sh (SPM + CocoaPods paths), with POSTHOG_INCLUDE_SOURCE=1 — without it traces resolve symbol names but show no source code. The script already handles Xcode's minimal PATH (nvm/Homebrew/npm), Release gating, min CLI version, and release association from build settings. - The only direct CLI form is `posthog-cli dsym upload --directory`; `posthog-cli upload ios` / --api-key / --dsym-path do not exist. - POSTHOG_CLI_HOST is the API host, never the *.i.posthog.com ingestion host from the app's PostHogConfig. - Required build settings: DEBUG_INFORMATION_FORMAT = dwarf-with-dsym (Release) and ENABLE_USER_SCRIPT_SANDBOXING = NO. - Swift test affordance is PostHogSDK.shared.captureException(error); capture(error) does not compile. Release run required to exercise the upload. - CocoaPods: PostHog.xcconfig must `#include?` the Pods xcconfig before taking over baseConfigurationReference. Contract test locks every pin against regression. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../description.md | 33 ++++++++++++++++--- .../error-tracking-upload-source-maps.test.js | 30 +++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 48d14e24..7670e234 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -37,6 +37,16 @@ Wire source map generation, chunk-ID injection, and upload into your **productio #### Examples - **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 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 JS source maps, and the posthog-ios SDK ships the upload script — do NOT hand-roll a `posthog-cli` invocation. Three required build-config changes on the app target: + 1. Release configuration emits dSYMs: `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` ("DWARF with dSYM File"). + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO` — the upload script traverses dSYM bundles and execs `posthog-cli`; with sandboxing on, script phases fail with "Operation not permitted" (and CocoaPods framework-embed phases break the build outright). + 3. A **Run Script phase, ordered last** (after "Embed Pods Frameworks" / "Copy Bundle Resources"), calling the script bundled with the SDK, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` added to the phase's **Input Files** (makes Xcode wait for the dSYM): + - SPM: `POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + + `upload-symbols.sh` already solves what hand-rolled scripts get wrong: it locates `posthog-cli` despite Xcode's minimal PATH (nvm, npm-global, Homebrew, cargo installs), skips non-Release configurations, enforces the minimum CLI version, and associates the release from `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`. Always prefix `POSTHOG_INCLUDE_SOURCE=1` — without it stack traces resolve to symbol names but show **no source code**. + + If the bundled script is genuinely unavailable, the ONLY valid direct command is `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source`. There is no `posthog-cli upload ios` subcommand and no `--api-key` / `--dsym-path` flags — do not invent CLI syntax; credentials come from the `POSTHOG_CLI_*` environment (see "Make credentials available at build time"). Known wrinkle: when a later version re-uploads an unchanged framework dSYM, the server may return `release_id_mismatch` and the CLI retries without release association — harmless for symbolication. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -50,7 +60,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) -- **iOS / Xcode does NOT use `.env`.** The dSYM upload runs as an Xcode **Run Script build phase** and Xcode never loads a `.env`. Put the secret personal API key in a **gitignored `.xcconfig`** assigned to the target's build configuration — Xcode exports every build setting into the Run Script phase's environment, so the script sees `$POSTHOG_CLI_API_KEY`. Keep the non-secret `POSTHOG_CLI_PROJECT_ID` / `POSTHOG_CLI_HOST` as `export` lines in the Run Script phase itself, **not** in the xcconfig — in xcconfig `//` begins a comment and would truncate `https://…`. +- **iOS / Xcode does NOT use `.env`.** The dSYM upload runs as an Xcode **Run Script build phase** and Xcode never loads a `.env`. Put the secret personal API key in a **gitignored `.xcconfig`** assigned to the target's build configuration — Xcode exports every build setting into the Run Script phase's environment, so the script sees `$POSTHOG_CLI_API_KEY`. Keep the non-secret `POSTHOG_CLI_PROJECT_ID` / `POSTHOG_CLI_HOST` as `export` lines in the Run Script phase itself, **not** in the xcconfig — in xcconfig `//` begins a comment and would truncate `https://…`. And `POSTHOG_CLI_HOST` is the **API host** (`https://us.posthog.com` or `https://eu.posthog.com`) — never the `*.i.posthog.com` ingestion host from the app's `PostHogConfig`; do not copy the host out of the SDK setup code. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -76,12 +86,16 @@ The upload credentials must be readable **by the build pipeline at build time**, ``` POSTHOG_CLI_API_KEY = phx_your_personal_api_key ``` - 2. Assign `PostHog.xcconfig` to the target's build configuration (Project ▸ Info ▸ Configurations, or `baseConfigurationReference` in the pbxproj). Xcode then exposes `POSTHOG_CLI_API_KEY` to Run Script phases as an environment variable. - 3. In the dSYM-upload Run Script phase, export the two non-secret values before invoking the script: + If the target already has a base configuration (CocoaPods projects: `Pods-..xcconfig`), the first line of `PostHog.xcconfig` must chain it, or the Pods settings are lost: + ``` + #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" + ``` + 2. Assign `PostHog.xcconfig` to the target's build configuration (Project ▸ Info ▸ Configurations, or `baseConfigurationReference` in the pbxproj — the Release configuration is the one the upload runs under). Xcode then exposes `POSTHOG_CLI_API_KEY` to Run Script phases as an environment variable. + 3. In the dSYM-upload Run Script phase, export the two non-secret values before invoking the script (SPM path shown; CocoaPods: `"${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"`): ```bash export POSTHOG_CLI_PROJECT_ID=12345 export POSTHOG_CLI_HOST=https://us.posthog.com - "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" + POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" ``` In CI, do **not** ship the xcconfig — set all three `POSTHOG_CLI_*` as job/environment secrets (Xcode Cloud environment variables, Fastlane `ENV`, GitHub Actions `env:`). Real environment variables take precedence and the build phase inherits them. @@ -290,7 +304,16 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex - **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.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 `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. - **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick captures a `Throwable` via the PostHog SDK, per the reference. -- **iOS (Swift)** Add a `UIButton` on the root view controller whose action captures an `NSError` via the PostHog SDK, per the reference. +- **iOS (Swift)** The capture method is `PostHogSDK.shared.captureException(error)` — `capture()` takes an event-name `String`, so `capture(error)` does not compile ("Cannot convert value of type 'any Error' to expected argument type 'String'"). SwiftUI: a `Button` on the root view; UIKit: a `UIButton` on the root view controller. Example handler: + ```swift + do { + throw NSError(domain: "PostHogSourceMapTest", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Source map upload test error"]) + } catch { + PostHogSDK.shared.captureException(error) + } + ``` + Testing the upload needs a **Release** run (dSYMs + the upload phase only exist there): Product ▸ Scheme ▸ Edit Scheme ▸ Run ▸ Build Configuration ▸ Release. - **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(Exception("PostHog source maps test"))`. ### Verify and hand off diff --git a/scripts/lib/tests/error-tracking-upload-source-maps.test.js b/scripts/lib/tests/error-tracking-upload-source-maps.test.js index f184874d..b249313a 100644 --- a/scripts/lib/tests/error-tracking-upload-source-maps.test.js +++ b/scripts/lib/tests/error-tracking-upload-source-maps.test.js @@ -53,4 +53,34 @@ describe('error-tracking-upload-source-maps iOS variant', () => { // not fall back to the dotenv flow. expect(SKILL_BODY).toMatch(/\*\*iOS exception\*\*/); }); + + it('pins the verified iOS upload pipeline so agents cannot re-hallucinate it', () => { + // Every assertion below is a mistake an agent actually made in a live + // run (2026-07-15) before the skill pinned the correct form. + + // 1. The canonical upload path is the SDK's bundled script, with + // source bundling on — otherwise traces resolve names but no code. + expect(SKILL_BODY).toContain('upload-symbols.sh'); + expect(SKILL_BODY).toContain('POSTHOG_INCLUDE_SOURCE=1'); + + // 2. The only real CLI command (agent invented `posthog-cli upload ios` + // with --api-key/--dsym-path flags). + expect(SKILL_BODY).toContain('posthog-cli dsym upload'); + expect(SKILL_BODY).toContain('no `posthog-cli upload ios` subcommand'); + + // 3. API host, not the SDK's ingestion host. + expect(SKILL_BODY).toMatch(/`\*\.i\.posthog\.com` ingestion host/); + + // 4. Build-settings prerequisites for the upload phase. + expect(SKILL_BODY).toContain('ENABLE_USER_SCRIPT_SANDBOXING = NO'); + expect(SKILL_BODY).toContain('dwarf-with-dsym'); + + // 5. Swift test affordance uses captureException; capture(error) + // does not compile. + expect(SKILL_BODY).toContain('PostHogSDK.shared.captureException(error)'); + + // CocoaPods base-config chaining — overwriting the Pods xcconfig + // breaks the build. + expect(SKILL_BODY).toContain('#include?'); + }); }); From 2e42624a5cf8e1c1e882660bc408b7c898d7e787 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 12:56:48 +0200 Subject: [PATCH 04/16] feat: pin xcconfig chaining direction and credential-fallback traps for iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second live run inverted the CocoaPods chaining — a Podfile post_install injecting `#include? "../../PostHog.xcconfig"` into the generated Pods xcconfig. Three stacked failures, all silent: the injection only lands on the next pod install (which never ran), the relative path was one level short (includes resolve from the including file's directory, so the project root is ../../../ from Target Support Files/Pods-/), and #include? swallows wrong paths. With no key in the build, posthog-cli fell back to stale ~/.posthog login credentials and failed with permission_denied instead of "no credentials found". Pin the correct direction (PostHog.xcconfig owns baseConfigurationReference and chains the Pods file), the path arithmetic, the showBuildSettings verification command, the ~/.posthog fallback trap, and REQUIRED language on POSTHOG_INCLUDE_SOURCE=1 (dropped again this run). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../description.md | 3 +++ .../error-tracking-upload-source-maps.test.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 7670e234..28c99e3a 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -90,6 +90,7 @@ The upload credentials must be readable **by the build pipeline at build time**, ``` #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" ``` + Wire it exactly this way round — `PostHog.xcconfig` at the project root includes the Pods file and takes `baseConfigurationReference`. Do NOT invert it by injecting an `#include?` of `PostHog.xcconfig` into the generated Pods xcconfig from a Podfile `post_install` hook: the injection only lands after the next `pod install` (easy to forget), xcconfig includes resolve relative to the **including file's directory** — from `Pods/Target Support Files/Pods-/` the project root is `../../../`, not `../../` — and `#include?` swallows a wrong path **silently**, so the credentials just vanish. 2. Assign `PostHog.xcconfig` to the target's build configuration (Project ▸ Info ▸ Configurations, or `baseConfigurationReference` in the pbxproj — the Release configuration is the one the upload runs under). Xcode then exposes `POSTHOG_CLI_API_KEY` to Run Script phases as an environment variable. 3. In the dSYM-upload Run Script phase, export the two non-secret values before invoking the script (SPM path shown; CocoaPods: `"${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"`): ```bash @@ -97,6 +98,8 @@ The upload credentials must be readable **by the build pipeline at build time**, export POSTHOG_CLI_HOST=https://us.posthog.com POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" ``` + The `POSTHOG_INCLUDE_SOURCE=1` prefix is REQUIRED, not decorative — without it PostHog resolves symbol names but shows **no source code** in stack traces. + **Silent-fallback trap**: if the key never reaches the build, `posthog-cli` falls back to any `~/.posthog` login credentials on the machine, so the upload fails with `permission_denied` (or lands in the wrong project) instead of "no credentials found". Before blaming the key, verify the chain delivers it: `xcodebuild -showBuildSettings -configuration Release | grep POSTHOG_CLI_API_KEY`. In CI, do **not** ship the xcconfig — set all three `POSTHOG_CLI_*` as job/environment secrets (Xcode Cloud environment variables, Fastlane `ENV`, GitHub Actions `env:`). Real environment variables take precedence and the build phase inherits them. ### Write credentials to the env file diff --git a/scripts/lib/tests/error-tracking-upload-source-maps.test.js b/scripts/lib/tests/error-tracking-upload-source-maps.test.js index b249313a..1866c76b 100644 --- a/scripts/lib/tests/error-tracking-upload-source-maps.test.js +++ b/scripts/lib/tests/error-tracking-upload-source-maps.test.js @@ -82,5 +82,19 @@ describe('error-tracking-upload-source-maps iOS variant', () => { // CocoaPods base-config chaining — overwriting the Pods xcconfig // breaks the build. expect(SKILL_BODY).toContain('#include?'); + + // 6. Second live run (same day) inverted the chain via a Podfile + // post_install hook, got the relative path wrong, and never re-ran + // pod install — credentials silently vanished. Pin the direction, + // the path arithmetic, and the debugging affordances. + expect(SKILL_BODY).toContain('Do NOT invert'); + expect(SKILL_BODY).toContain('`../../../`, not `../../`'); + expect(SKILL_BODY).toContain('~/.posthog'); + expect(SKILL_BODY).toContain( + 'xcodebuild -showBuildSettings -configuration Release | grep POSTHOG_CLI_API_KEY', + ); + expect(SKILL_BODY).toContain( + '`POSTHOG_INCLUDE_SOURCE=1` prefix is REQUIRED', + ); }); }); From f309fb296b8eec9368b9fd9380330d8f49f3f95f Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 13:19:51 +0200 Subject: [PATCH 05/16] refactor: slim the iOS source-maps guidance to a minimal shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut the war-story prose and everything the snippets already show; each fact now appears once. Drop the skill-body content tests — the original skill-contract test stays. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../description.md | 42 +++++------ .../error-tracking-upload-source-maps.test.js | 70 ------------------- 2 files changed, 17 insertions(+), 95 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 28c99e3a..9a3ab280 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -37,16 +37,13 @@ Wire source map generation, chunk-ID injection, and upload into your **productio #### Examples - **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 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 JS source maps, and the posthog-ios SDK ships the upload script — do NOT hand-roll a `posthog-cli` invocation. Three required build-config changes on the app target: - 1. Release configuration emits dSYMs: `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` ("DWARF with dSYM File"). - 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO` — the upload script traverses dSYM bundles and execs `posthog-cli`; with sandboxing on, script phases fail with "Operation not permitted" (and CocoaPods framework-embed phases break the build outright). - 3. A **Run Script phase, ordered last** (after "Embed Pods Frameworks" / "Copy Bundle Resources"), calling the script bundled with the SDK, with `$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(EXECUTABLE_NAME)` added to the phase's **Input Files** (makes Xcode wait for the dSYM): +- **iOS (Xcode)** iOS uploads **dSYM debug symbols**, not source maps. Required target changes: + 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO` (script phases fail with "Operation not permitted" otherwise). + 3. 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 — it handles PATH lookup, Release gating, CLI version, and release association; do not hand-roll it: - SPM: `POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - - `upload-symbols.sh` already solves what hand-rolled scripts get wrong: it locates `posthog-cli` despite Xcode's minimal PATH (nvm, npm-global, Homebrew, cargo installs), skips non-Release configurations, enforces the minimum CLI version, and associates the release from `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`. Always prefix `POSTHOG_INCLUDE_SOURCE=1` — without it stack traces resolve to symbol names but show **no source code**. - - If the bundled script is genuinely unavailable, the ONLY valid direct command is `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source`. There is no `posthog-cli upload ios` subcommand and no `--api-key` / `--dsym-path` flags — do not invent CLI syntax; credentials come from the `POSTHOG_CLI_*` environment (see "Make credentials available at build time"). Known wrinkle: when a later version re-uploads an unchanged framework dSYM, the server may return `release_id_mismatch` and the CLI retries without release association — harmless for symbolication. + `POSTHOG_INCLUDE_SOURCE=1` bundles source so traces show code, not just symbol names. Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -60,7 +57,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) -- **iOS / Xcode does NOT use `.env`.** The dSYM upload runs as an Xcode **Run Script build phase** and Xcode never loads a `.env`. Put the secret personal API key in a **gitignored `.xcconfig`** assigned to the target's build configuration — Xcode exports every build setting into the Run Script phase's environment, so the script sees `$POSTHOG_CLI_API_KEY`. Keep the non-secret `POSTHOG_CLI_PROJECT_ID` / `POSTHOG_CLI_HOST` as `export` lines in the Run Script phase itself, **not** in the xcconfig — in xcconfig `//` begins a comment and would truncate `https://…`. And `POSTHOG_CLI_HOST` is the **API host** (`https://us.posthog.com` or `https://eu.posthog.com`) — never the `*.i.posthog.com` ingestion host from the app's `PostHogConfig`; do not copy the host out of the SDK setup code. +- **iOS / Xcode does NOT use `.env`.** The secret key goes in a **gitignored `.xcconfig`** set as the target's base configuration — Xcode exports build settings into Run Script phase environments. Non-secret project id / host are `export` lines in the phase itself (in xcconfig `//` starts a comment, truncating URLs). `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com` / `https://eu.posthog.com`) — never the `*.i.posthog.com` ingestion host from the app's `PostHogConfig`. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -81,26 +78,21 @@ The upload credentials must be readable **by the build pipeline at build time**, ```json "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" ``` -- **iOS (Xcode / posthog-cli)** The posthog-ios SDK ships `upload-symbols.sh`, run as a **Run Script build phase**; it calls `posthog-cli`, which reads `POSTHOG_CLI_*` from the environment. There is no `.env` — wire the credentials through Xcode instead: - 1. Create a **gitignored** `PostHog.xcconfig` holding only the secret (commit a `PostHog.example.xcconfig` with an empty value so teammates see the setting exists): - ``` - POSTHOG_CLI_API_KEY = phx_your_personal_api_key - ``` - If the target already has a base configuration (CocoaPods projects: `Pods-..xcconfig`), the first line of `PostHog.xcconfig` must chain it, or the Pods settings are lost: +- **iOS (Xcode / posthog-cli)** No `.env` — wire credentials through Xcode: + 1. Gitignored `PostHog.xcconfig` holding only the secret (commit an empty `PostHog.example.xcconfig` so teammates see the setting): ``` #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" + POSTHOG_CLI_API_KEY = phx_your_personal_api_key ``` - Wire it exactly this way round — `PostHog.xcconfig` at the project root includes the Pods file and takes `baseConfigurationReference`. Do NOT invert it by injecting an `#include?` of `PostHog.xcconfig` into the generated Pods xcconfig from a Podfile `post_install` hook: the injection only lands after the next `pod install` (easy to forget), xcconfig includes resolve relative to the **including file's directory** — from `Pods/Target Support Files/Pods-/` the project root is `../../../`, not `../../` — and `#include?` swallows a wrong path **silently**, so the credentials just vanish. - 2. Assign `PostHog.xcconfig` to the target's build configuration (Project ▸ Info ▸ Configurations, or `baseConfigurationReference` in the pbxproj — the Release configuration is the one the upload runs under). Xcode then exposes `POSTHOG_CLI_API_KEY` to Run Script phases as an environment variable. - 3. In the dSYM-upload Run Script phase, export the two non-secret values before invoking the script (SPM path shown; CocoaPods: `"${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"`): + The `#include?` line is CocoaPods-only — it chains the existing base config, which step 2 displaces. Chain in this direction; do NOT inject an include of `PostHog.xcconfig` into the generated Pods file via `post_install` (needs a re-run of `pod install`, and a wrong relative path fails silently). SPM projects drop the line. + 2. Set `PostHog.xcconfig` as the Release configuration's `baseConfigurationReference` (Project ▸ Info ▸ Configurations). + 3. Export the non-secret values in the upload Run Script phase, before the script: ```bash export POSTHOG_CLI_PROJECT_ID=12345 export POSTHOG_CLI_HOST=https://us.posthog.com - POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh" ``` - The `POSTHOG_INCLUDE_SOURCE=1` prefix is REQUIRED, not decorative — without it PostHog resolves symbol names but shows **no source code** in stack traces. - **Silent-fallback trap**: if the key never reaches the build, `posthog-cli` falls back to any `~/.posthog` login credentials on the machine, so the upload fails with `permission_denied` (or lands in the wrong project) instead of "no credentials found". Before blaming the key, verify the chain delivers it: `xcodebuild -showBuildSettings -configuration Release | grep POSTHOG_CLI_API_KEY`. - In CI, do **not** ship the xcconfig — set all three `POSTHOG_CLI_*` as job/environment secrets (Xcode Cloud environment variables, Fastlane `ENV`, GitHub Actions `env:`). Real environment variables take precedence and the build phase inherits them. + In CI, set all three `POSTHOG_CLI_*` as job secrets instead — real env vars take precedence; no xcconfig on the runner. + `permission_denied` on upload usually means the key never reached the build and the CLI fell back to `~/.posthog` login credentials — check `xcodebuild -showBuildSettings -configuration Release | grep POSTHOG_CLI_API_KEY`. ### Write credentials to the env file @@ -108,7 +100,7 @@ Write the personal API key and project identifiers into the env file your build #### 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. -- **iOS exception**: there is no env file. Write only the secret `POSTHOG_CLI_API_KEY` to a gitignored `.xcconfig` (see "Make credentials available at build time"); the project id and host are exported in the Run Script phase, not written to a file. +- **iOS exception**: no env file — secret to the gitignored `.xcconfig`, project id / host exported in the Run Script phase (see "Make credentials available at build time"). - Variable names depend on which uploader you wired: - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` @@ -307,7 +299,7 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex - **Node.js** Add a temporary route (e.g. `GET /__posthog-test-error`) on the existing server that calls `posthog.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 `Button` on the main screen whose onPress calls `posthog.captureException(new Error("PostHog source maps test"))`. - **Android (Kotlin)** Add a `Button` on the launcher Activity whose onClick captures a `Throwable` via the PostHog SDK, per the reference. -- **iOS (Swift)** The capture method is `PostHogSDK.shared.captureException(error)` — `capture()` takes an event-name `String`, so `capture(error)` does not compile ("Cannot convert value of type 'any Error' to expected argument type 'String'"). SwiftUI: a `Button` on the root view; UIKit: a `UIButton` on the root view controller. Example handler: +- **iOS (Swift)** `Button` on the root view (SwiftUI) or `UIButton` on the root view controller (UIKit), handler: ```swift do { throw NSError(domain: "PostHogSourceMapTest", code: 1, @@ -316,7 +308,7 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex PostHogSDK.shared.captureException(error) } ``` - Testing the upload needs a **Release** run (dSYMs + the upload phase only exist there): Product ▸ Scheme ▸ Edit Scheme ▸ Run ▸ Build Configuration ▸ Release. + (`capture()` takes an event-name String, not an Error.) Testing needs a **Release** run: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release. - **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(Exception("PostHog source maps test"))`. ### Verify and hand off diff --git a/scripts/lib/tests/error-tracking-upload-source-maps.test.js b/scripts/lib/tests/error-tracking-upload-source-maps.test.js index 1866c76b..987afd96 100644 --- a/scripts/lib/tests/error-tracking-upload-source-maps.test.js +++ b/scripts/lib/tests/error-tracking-upload-source-maps.test.js @@ -1,19 +1,9 @@ import { describe, expect, it } from 'vitest'; import { join } from 'path'; -import { readFileSync } from 'fs'; import { expandSkillGroups, loadSkillsConfig } from '../skill-generator.js'; const CONFIG_DIR = join(process.cwd(), 'context'); -const SKILL_BODY = readFileSync( - join( - CONFIG_DIR, - 'skills', - 'error-tracking-upload-source-maps', - 'description.md', - ), - 'utf8', -); describe('error-tracking-upload-source-maps iOS variant', () => { it('expands to the skill contract consumed by the wizard', () => { @@ -37,64 +27,4 @@ describe('error-tracking-upload-source-maps iOS variant', () => { _cli: null, }); }); - - it('routes iOS build-time credentials through a gitignored xcconfig, not .env', () => { - // iOS never loads a .env at build time — the secret must land in an - // xcconfig surfaced to the Run Script phase, and the non-secret values - // stay out of that file (host contains `//`, which comments in xcconfig). - expect(SKILL_BODY).toMatch(/iOS \/ Xcode does NOT use `\.env`/); - expect(SKILL_BODY).toMatch(/gitignored/); - expect(SKILL_BODY).toContain('PostHog.xcconfig'); - expect(SKILL_BODY).toContain('POSTHOG_CLI_API_KEY = phx_'); - expect(SKILL_BODY).toMatch( - /export POSTHOG_CLI_PROJECT_ID[\s\S]*export POSTHOG_CLI_HOST/, - ); - // The secret write step must flag the iOS exception so the agent does - // not fall back to the dotenv flow. - expect(SKILL_BODY).toMatch(/\*\*iOS exception\*\*/); - }); - - it('pins the verified iOS upload pipeline so agents cannot re-hallucinate it', () => { - // Every assertion below is a mistake an agent actually made in a live - // run (2026-07-15) before the skill pinned the correct form. - - // 1. The canonical upload path is the SDK's bundled script, with - // source bundling on — otherwise traces resolve names but no code. - expect(SKILL_BODY).toContain('upload-symbols.sh'); - expect(SKILL_BODY).toContain('POSTHOG_INCLUDE_SOURCE=1'); - - // 2. The only real CLI command (agent invented `posthog-cli upload ios` - // with --api-key/--dsym-path flags). - expect(SKILL_BODY).toContain('posthog-cli dsym upload'); - expect(SKILL_BODY).toContain('no `posthog-cli upload ios` subcommand'); - - // 3. API host, not the SDK's ingestion host. - expect(SKILL_BODY).toMatch(/`\*\.i\.posthog\.com` ingestion host/); - - // 4. Build-settings prerequisites for the upload phase. - expect(SKILL_BODY).toContain('ENABLE_USER_SCRIPT_SANDBOXING = NO'); - expect(SKILL_BODY).toContain('dwarf-with-dsym'); - - // 5. Swift test affordance uses captureException; capture(error) - // does not compile. - expect(SKILL_BODY).toContain('PostHogSDK.shared.captureException(error)'); - - // CocoaPods base-config chaining — overwriting the Pods xcconfig - // breaks the build. - expect(SKILL_BODY).toContain('#include?'); - - // 6. Second live run (same day) inverted the chain via a Podfile - // post_install hook, got the relative path wrong, and never re-ran - // pod install — credentials silently vanished. Pin the direction, - // the path arithmetic, and the debugging affordances. - expect(SKILL_BODY).toContain('Do NOT invert'); - expect(SKILL_BODY).toContain('`../../../`, not `../../`'); - expect(SKILL_BODY).toContain('~/.posthog'); - expect(SKILL_BODY).toContain( - 'xcodebuild -showBuildSettings -configuration Release | grep POSTHOG_CLI_API_KEY', - ); - expect(SKILL_BODY).toContain( - '`POSTHOG_INCLUDE_SOURCE=1` prefix is REQUIRED', - ); - }); }); From 5296059ff7a3e0641b17773abf6246ee764b1089 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 13:38:58 +0200 Subject: [PATCH 06/16] fix: iOS local test is one Xcode step, not a CLI/GUI mix Build+run for local testing = Xcode Run in Release; xcodebuild is CI-only. Note the test captures an event, not a crash, so no debugger-detach or relaunch steps. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/error-tracking-upload-source-maps/description.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 9a3ab280..c62a38ac 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -120,7 +120,7 @@ Resolve two concrete commands for this project: the production **build** command - **Vite** Build: `npm run build`. Run: `npm run preview`. - **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and 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** Build: `xcodebuild …`. Run: launch the scheme on a simulator/device (Xcode). +- **iOS** Local build + run are one step: Xcode Run with the scheme's Build Configuration set to Release (the build phase uploads dSYMs). `xcodebuild` is for CI, not local testing. - **Flutter** Build: `flutter build apk` / `flutter build ios`. Run: `flutter run`. - **React Native** Run: `npx react-native run-ios` / `npx react-native run-android`. @@ -308,7 +308,7 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex PostHogSDK.shared.captureException(error) } ``` - (`capture()` takes an event-name String, not an Error.) Testing needs a **Release** run: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release. + (`capture()` takes an event-name String, not an Error.) Testing needs a **Release** run: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release. This captures an event, not a crash — no debugger-detach or relaunch steps. - **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(Exception("PostHog source maps test"))`. ### Verify and hand off From a012d1ddbf0537c727f6f746404c3d66219b1046 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 14:44:17 +0200 Subject: [PATCH 07/16] fix: tie POSTHOG_INCLUDE_SOURCE=1 to its failure symptom Agents dropped the prefix in two consecutive runs; state that it is part of the command and name the exact symptom omission causes. Co-Authored-By: Claude Opus 4.8 (1M context) --- context/skills/error-tracking-upload-source-maps/description.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index c62a38ac..11744c93 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -43,7 +43,7 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 3. 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 — it handles PATH lookup, Release gating, CLI version, and release association; do not hand-roll it: - SPM: `POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - `POSTHOG_INCLUDE_SOURCE=1` bundles source so traces show code, not just symbol names. Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. + The `POSTHOG_INCLUDE_SOURCE=1` prefix is part of the command — dropping it is exactly the "frame resolved but source context not available" symptom (symbols upload, source never does). Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. From 2ba7c3deadc5ee7d3544835e1af17e58812238e3 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 14:45:18 +0200 Subject: [PATCH 08/16] refactor: trim include-source note to one clause Co-Authored-By: Claude Opus 4.8 (1M context) --- context/skills/error-tracking-upload-source-maps/description.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 11744c93..a46dd063 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -43,7 +43,7 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 3. 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 — it handles PATH lookup, Release gating, CLI version, and release association; do not hand-roll it: - SPM: `POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - The `POSTHOG_INCLUDE_SOURCE=1` prefix is part of the command — dropping it is exactly the "frame resolved but source context not available" symptom (symbols upload, source never does). Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. + The `POSTHOG_INCLUDE_SOURCE=1` prefix is part of the command, not optional. Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. From 19a959a9034f8f07a6a9addcb77ddb25f0e70dc7 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 14:46:00 +0200 Subject: [PATCH 09/16] refactor: mandate the include-source prefix on the snippet itself Co-Authored-By: Claude Opus 4.8 (1M context) --- context/skills/error-tracking-upload-source-maps/description.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index a46dd063..2b152ce3 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -43,7 +43,7 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 3. 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 — it handles PATH lookup, Release gating, CLI version, and release association; do not hand-roll it: - SPM: `POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - The `POSTHOG_INCLUDE_SOURCE=1` prefix is part of the command, not optional. Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` prefix HAS to be there. Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. From a4736d2ffd014a5721080935af2eaa8c2e0fa829 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 15:00:17 +0200 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20minimal=20iOS=20shell=20?= =?UTF-8?q?=E2=80=94=20all=20creds=20in=20one=20xcconfig,=20no=20fallbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Single gitignored xcconfig carries key + project id + host (host with the $() escape); the phase-exports step and its snippet are gone. - Drop the direct-CLI fallback, the post_install inversion warning, and the permission_denied diagnostic — the examples show the one right way. - Trim run-command and test bullets; scope test-revert to the affordance only so credential wiring survives. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../description.md | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 2b152ce3..56fdf470 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -39,11 +39,11 @@ Wire source map generation, chunk-ID injection, and upload into your **productio - **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: 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. - 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO` (script phases fail with "Operation not permitted" otherwise). - 3. 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 — it handles PATH lookup, Release gating, CLI version, and release association; do not hand-roll it: + 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. + 3. 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 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` prefix HAS to be there. Direct CLI fallback: `posthog-cli dsym upload --directory "${DWARF_DSYM_FOLDER_PATH}" --main-dsym "${DWARF_DSYM_FILE_NAME}" --include-source` — there is no `upload ios` subcommand, no `--api-key` / `--dsym-path` flags. + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` prefix HAS to be there. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -57,7 +57,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) -- **iOS / Xcode does NOT use `.env`.** The secret key goes in a **gitignored `.xcconfig`** set as the target's base configuration — Xcode exports build settings into Run Script phase environments. Non-secret project id / host are `export` lines in the phase itself (in xcconfig `//` starts a comment, truncating URLs). `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com` / `https://eu.posthog.com`) — never the `*.i.posthog.com` ingestion host from the app's `PostHogConfig`. +- **iOS / Xcode does NOT use `.env`** — all three `POSTHOG_CLI_*` values go in a gitignored `.xcconfig` (see the iOS example). `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -78,21 +78,14 @@ The upload credentials must be readable **by the build pipeline at build time**, ```json "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" ``` -- **iOS (Xcode / posthog-cli)** No `.env` — wire credentials through Xcode: - 1. Gitignored `PostHog.xcconfig` holding only the secret (commit an empty `PostHog.example.xcconfig` so teammates see the setting): - ``` - #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" - POSTHOG_CLI_API_KEY = phx_your_personal_api_key - ``` - The `#include?` line is CocoaPods-only — it chains the existing base config, which step 2 displaces. Chain in this direction; do NOT inject an include of `PostHog.xcconfig` into the generated Pods file via `post_install` (needs a re-run of `pod install`, and a wrong relative path fails silently). SPM projects drop the line. - 2. Set `PostHog.xcconfig` as the Release configuration's `baseConfigurationReference` (Project ▸ Info ▸ Configurations). - 3. Export the non-secret values in the upload Run Script phase, before the script: - ```bash - export POSTHOG_CLI_PROJECT_ID=12345 - export POSTHOG_CLI_HOST=https://us.posthog.com - ``` - In CI, set all three `POSTHOG_CLI_*` as job secrets instead — real env vars take precedence; no xcconfig on the runner. - `permission_denied` on upload usually means the key never reached the build and the CLI fell back to `~/.posthog` login credentials — check `xcodebuild -showBuildSettings -configuration Release | grep POSTHOG_CLI_API_KEY`. +- **iOS (Xcode / posthog-cli)** No `.env` — a **gitignored** `PostHog.xcconfig`, set as the Release configuration's `baseConfigurationReference` (Project ▸ Info ▸ Configurations), carries all three values. Commit an empty `PostHog.example.xcconfig` so teammates see the settings: + ``` + #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" + POSTHOG_CLI_API_KEY = phx_your_personal_api_key + POSTHOG_CLI_PROJECT_ID = 12345 + POSTHOG_CLI_HOST = https:/$()/us.posthog.com + ``` + The `#include?` line is CocoaPods-only (it chains the base config this file displaces); SPM projects drop it. The `$()` splits `//`, which would otherwise start an xcconfig comment. In CI, set the three `POSTHOG_CLI_*` as job secrets instead — no xcconfig on the runner. ### Write credentials to the env file @@ -120,7 +113,7 @@ Resolve two concrete commands for this project: the production **build** command - **Vite** Build: `npm run build`. Run: `npm run preview`. - **Plain Node** Build: `npm run build`. Run: `node ` — read package.json `main`/`bin` and 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 the scheme's Build Configuration set to Release (the build phase uploads dSYMs). `xcodebuild` is for CI, not local testing. +- **iOS** Local build + run are one step: Xcode Run with Build Configuration = Release. `xcodebuild` is 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`. @@ -291,7 +284,7 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex - 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 touched file and re-read to confirm nothing is left behind. Never leave the affordance in place — even if the test "didn't work", revert first. +- 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 @@ -308,7 +301,7 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex PostHogSDK.shared.captureException(error) } ``` - (`capture()` takes an event-name String, not an Error.) Testing needs a **Release** run: Edit Scheme ▸ Run ▸ Build Configuration ▸ Release. This captures an event, not a crash — no debugger-detach or relaunch steps. + (`capture()` takes an event-name String, not an Error.) Test with a **Release** run (Edit Scheme ▸ Run ▸ Release); it's an event, not a crash — no debugger or relaunch steps. - **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(Exception("PostHog source maps test"))`. ### Verify and hand off From d0c790ac97bf39d6420bc005c688f0c3346ce725 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 15:19:09 +0200 Subject: [PATCH 11/16] fix: include-source flag lives in the xcconfig; fileRef in the root group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents dropped the POSTHOG_INCLUDE_SOURCE=1 command-line prefix three runs straight — as a build setting in the xcconfig it reaches the script environment with nothing to copy. Pin the file reference to the project ROOT group: under the Pods group it resolves to Pods/PostHog.xcconfig, which kills the base-config chain and the build. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../description.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 56fdf470..ff57ec14 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -41,9 +41,9 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. 3. 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 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` prefix HAS to be there. + - SPM: `"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `"${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Source bundling comes from the `POSTHOG_INCLUDE_SOURCE = 1` line in the xcconfig ("Make credentials available at build time"). - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -57,7 +57,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) -- **iOS / Xcode does NOT use `.env`** — all three `POSTHOG_CLI_*` values go in a gitignored `.xcconfig` (see the iOS example). `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **iOS / Xcode does NOT use `.env`** — the whole config goes in a gitignored `.xcconfig` (see the iOS example). `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -78,14 +78,15 @@ The upload credentials must be readable **by the build pipeline at build time**, ```json "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" ``` -- **iOS (Xcode / posthog-cli)** No `.env` — a **gitignored** `PostHog.xcconfig`, set as the Release configuration's `baseConfigurationReference` (Project ▸ Info ▸ Configurations), carries all three values. Commit an empty `PostHog.example.xcconfig` so teammates see the settings: +- **iOS (Xcode / posthog-cli)** No `.env` — a **gitignored** `PostHog.xcconfig` at the project root, set as the Release configuration's `baseConfigurationReference` (Project ▸ Info ▸ Configurations), carries the whole config. Its pbxproj file reference goes in the ROOT group — under the Pods group it resolves to `Pods/PostHog.xcconfig` and breaks the build. Commit an empty `PostHog.example.xcconfig` so teammates see the settings: ``` #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" POSTHOG_CLI_API_KEY = phx_your_personal_api_key POSTHOG_CLI_PROJECT_ID = 12345 POSTHOG_CLI_HOST = https:/$()/us.posthog.com + POSTHOG_INCLUDE_SOURCE = 1 ``` - The `#include?` line is CocoaPods-only (it chains the base config this file displaces); SPM projects drop it. The `$()` splits `//`, which would otherwise start an xcconfig comment. In CI, set the three `POSTHOG_CLI_*` as job secrets instead — no xcconfig on the runner. + The `#include?` line is CocoaPods-only (it chains the base config this file displaces); SPM projects drop it. The `$()` splits `//`, which would otherwise start an xcconfig comment. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no xcconfig on the runner. ### Write credentials to the env file From 3eb8fc34bdcad7dac7e65a8602977e3ae99372c0 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 17:36:31 +0200 Subject: [PATCH 12/16] revert: include-source stays an inline prefix on the invocation Co-Authored-By: Claude Opus 4.8 (1M context) --- .../error-tracking-upload-source-maps/description.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index ff57ec14..2bf36484 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -41,9 +41,9 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. 3. 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: `"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - - CocoaPods: `"${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - Source bundling comes from the `POSTHOG_INCLUDE_SOURCE = 1` line in the xcconfig ("Make credentials available at build time"). + - SPM: `POSTHOG_INCLUDE_SOURCE=1 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` prefix HAS to be there. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -84,7 +84,6 @@ The upload credentials must be readable **by the build pipeline at build time**, POSTHOG_CLI_API_KEY = phx_your_personal_api_key POSTHOG_CLI_PROJECT_ID = 12345 POSTHOG_CLI_HOST = https:/$()/us.posthog.com - POSTHOG_INCLUDE_SOURCE = 1 ``` The `#include?` line is CocoaPods-only (it chains the base config this file displaces); SPM projects drop it. The `$()` splits `//`, which would otherwise start an xcconfig comment. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no xcconfig on the runner. From 1d03ec5aa228b6304a28e5dec5f49c579953376f Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 19:29:33 +0200 Subject: [PATCH 13/16] feat(ios): credentials move from xcconfig to gitignored .env upload-symbols.sh (posthog-ios) now forwards POSTHOG_DOTENV_FILE to posthog-cli --dotenv-file, so iOS uses the same .env flow as every other platform. Drops the xcconfig example, baseConfigurationReference wiring, and the #include? chaining story. Co-Authored-By: Claude Fable 5 --- .../description.md | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 2bf36484..a72ff2e7 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -41,9 +41,9 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. 3. 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 "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` prefix HAS to be there. + - SPM: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` + - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_DOTENV_FILE` prefixes HAVE to be there. Update posthog-ios to the latest version first — older `upload-symbols.sh` silently ignores `POSTHOG_DOTENV_FILE`. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -57,7 +57,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-cli login`, which leaves credentials in `~/.posthog` — that won't exist in CI or on a teammate's machine.) -- **iOS / Xcode does NOT use `.env`** — the whole config goes in a gitignored `.xcconfig` (see the iOS example). `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **iOS / Xcode** No loader — the Run Script phase's `POSTHOG_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -78,14 +78,7 @@ The upload credentials must be readable **by the build pipeline at build time**, ```json "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" ``` -- **iOS (Xcode / posthog-cli)** No `.env` — a **gitignored** `PostHog.xcconfig` at the project root, set as the Release configuration's `baseConfigurationReference` (Project ▸ Info ▸ Configurations), carries the whole config. Its pbxproj file reference goes in the ROOT group — under the Pods group it resolves to `Pods/PostHog.xcconfig` and breaks the build. Commit an empty `PostHog.example.xcconfig` so teammates see the settings: - ``` - #include? "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig" - POSTHOG_CLI_API_KEY = phx_your_personal_api_key - POSTHOG_CLI_PROJECT_ID = 12345 - POSTHOG_CLI_HOST = https:/$()/us.posthog.com - ``` - The `#include?` line is CocoaPods-only (it chains the base config this file displaces); SPM projects drop it. The `$()` splits `//`, which would otherwise start an xcconfig comment. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no xcconfig on the runner. +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. ### Write credentials to the env file @@ -93,7 +86,6 @@ Write the personal API key and project identifiers into the env file your build #### 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. -- **iOS exception**: no env file — secret to the gitignored `.xcconfig`, project id / host exported in the Run Script phase (see "Make credentials available at build time"). - Variable names depend on which uploader you wired: - `posthog-cli` direct upload → `POSTHOG_CLI_API_KEY`, `POSTHOG_CLI_PROJECT_ID`, `POSTHOG_CLI_HOST` - bundler-plugin variants → `POSTHOG_API_KEY`, `POSTHOG_PROJECT_ID`, `POSTHOG_HOST` From 6ae2731d6aa4e4b7b079c08d47677aa48be08861 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 19:44:47 +0200 Subject: [PATCH 14/16] fix(ios): dotenv path rides POSTHOG_CLI_DOTENV_FILE, read by the CLI itself The CLI-side env var (PostHog/posthog#71282) passes through every already-shipped upload-symbols.sh, so no posthog-ios release is needed; the gate moves to posthog-cli, which the step updates anyway. Co-Authored-By: Claude Fable 5 --- .../error-tracking-upload-source-maps/description.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index a72ff2e7..bc23757c 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -41,9 +41,9 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 1. `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` for Release. 2. `ENABLE_USER_SCRIPT_SANDBOXING = NO`. 3. 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_DOTENV_FILE="${SRCROOT}/.env" "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/posthog-ios/build-tools/upload-symbols.sh"` - - CocoaPods: `POSTHOG_INCLUDE_SOURCE=1 POSTHOG_DOTENV_FILE="${SRCROOT}/.env" "${PODS_ROOT}/PostHog/build-tools/upload-symbols.sh"` - Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_DOTENV_FILE` prefixes HAVE to be there. Update posthog-ios to the latest version first — older `upload-symbols.sh` silently ignores `POSTHOG_DOTENV_FILE`. + - 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 — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. Update posthog-cli first (`npm install -g @posthog/cli@latest`) — older CLIs silently ignore `POSTHOG_CLI_DOTENV_FILE`. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -57,7 +57,7 @@ The upload credentials must be readable **by the build pipeline at build time**, - **Does NOT auto-load `.env`**: Rollup, plain webpack, and plain Node scripts. Load it explicitly — add `dotenv` (`require('dotenv').config()`, or `import 'dotenv/config'` for ESM) at the top of the bundler/config file. - **Separate-process gotcha**: if `posthog-cli sourcemap process` runs as its own `package.json` step (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 sourcemap process …` (the flag goes before the subcommand). - **`process` authenticates from the start.** `posthog-cli sourcemap process` resolves credentials before it injects chunk IDs — the inject phase needs them too, not just the upload — and fails without them. Always pass `--dotenv-file` to the `process` invocation. (It can still appear to work if the developer once ran `posthog-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_DOTENV_FILE="${SRCROOT}/.env"` prefix points posthog-cli at the gitignored `.env`. `POSTHOG_CLI_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. +- **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_HOST` is the API host (`https://us.posthog.com`), never the `*.i.posthog.com` ingestion host. #### Examples - **Next.js / Nuxt** Auto-load `.env` at build time; put the vars there and you're done. @@ -78,7 +78,7 @@ The upload credentials must be readable **by the build pipeline at build time**, ```json "build": "tsc && posthog-cli --dotenv-file .env sourcemap process --directory ./dist --release-name my-app" ``` -- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. +- **iOS (Xcode / posthog-cli)** A gitignored `.env` next to the `.xcodeproj` — the Run Script invocation's `POSTHOG_CLI_DOTENV_FILE="${SRCROOT}/.env"` prefix hands it to posthog-cli. No Xcode project wiring beyond the Run Script phase. In CI, set the `POSTHOG_CLI_*` values as job secrets instead — no `.env` on the runner. ### Write credentials to the env file From e6ca679f126d4ba7fcf29701ce11870344001157 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 15 Jul 2026 20:56:58 +0200 Subject: [PATCH 15/16] chore(ios): drop redundant trailer after the test-snippet The Swift example already pins captureException, and the Release-run / no-debugger guidance lives in the "Identify the build and run commands" step and the wizard's own test-done prompt. Co-Authored-By: Claude Fable 5 --- context/skills/error-tracking-upload-source-maps/description.md | 1 - 1 file changed, 1 deletion(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index bc23757c..452c5619 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -293,7 +293,6 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex PostHogSDK.shared.captureException(error) } ``` - (`capture()` takes an event-name String, not an Error.) Test with a **Release** run (Edit Scheme ▸ Run ▸ Release); it's an event, not a crash — no debugger or relaunch steps. - **Flutter** Add an `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(Exception("PostHog source maps test"))`. ### Verify and hand off From 994d1da447c00dfd33c3ccb51b67537fa78d68ee Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Thu, 16 Jul 2026 16:05:12 +0200 Subject: [PATCH 16/16] fix: move stuff to context mill --- .../skills/error-tracking-upload-source-maps/description.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index 452c5619..5bbaaa3e 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -43,7 +43,7 @@ Wire source map generation, chunk-ID injection, and upload into your **productio 3. 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 — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. Update posthog-cli first (`npm install -g @posthog/cli@latest`) — older CLIs silently ignore `POSTHOG_CLI_DOTENV_FILE`. + Copy the invocation verbatim — the `POSTHOG_INCLUDE_SOURCE=1` and `POSTHOG_CLI_DOTENV_FILE` prefixes HAVE to be there. This needs a recent `posthog-cli` (older ones silently ignore `POSTHOG_CLI_DOTENV_FILE`); the PostHog wizard installs it for you, so do not run `npm install -g` yourself. - **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 / Android / iOS / Flutter** You upload platform debug symbols (Hermes maps, ProGuard/R8 mappings, dSYMs) rather than plain `.js.map` files — follow the platform reference for the exact build hook. @@ -293,6 +293,7 @@ Optionally add a temporary, clearly-labeled affordance that captures one test ex 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 (no `xcodebuild`): 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 `ElevatedButton` on the home widget whose onPressed calls `Posthog().captureException(Exception("PostHog source maps test"))`. ### Verify and hand off