diff --git a/context/agents/error-tracking/capture-exceptions.md b/context/agents/error-tracking/capture-exceptions.md new file mode 100644 index 00000000..d1f9da58 --- /dev/null +++ b/context/agents/error-tracking/capture-exceptions.md @@ -0,0 +1,69 @@ +--- +type: capture-exceptions +flow: error-tracking +label: Wire up exception capture +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [integration-v2-error-tracking-step, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Make the errors the app does not catch reach PostHog, by whatever means the +SDK offers for that. Which means depends on the SDK: some autocapture +exceptions once you enable it at init, some wire into the framework's own +error handler, some give you a boundary to mount at the app entry. Follow the +docs and the reference example for this one, and set it up in one place — +never manual capture calls sprinkled across files. + +A page often shows two setups side by side: a global initializer, and an +explicit client you hold. Follow one of them the whole way through. Taking the +wiring from one and the calls from the other reads fine and compiles nowhere — +an initializer that hands back nothing leaves you nothing to call methods on. + +The SDK is installed and initialized — either it already was, or the install +and init tasks before you did it (see their handoffs); build on that, do not +re-check it. + +This is an instrument-only task. Do not install dependencies, run the build, +run tests, or start the app — the user-driven test-setup step at the end of +the flow verifies, when the user wants it. Do not touch the build config +either way; when the flow includes a configure task, it owns those files. +Stay inside this project's directory and set up that one place; that is the +whole job. + +## Do not copy the SDK's own listeners + +Exception autocapture already registers the global handlers: `window.onerror` +and `unhandledrejection` in the browser, `uncaughtException` and +`unhandledRejection` in Node, `sys.excepthook` in Python, the panic hook in +Rust, and the uncaught-exception and signal handlers on iOS and Android. Turn +that on with the SDK's own option — `capture_exceptions`, +`enableExceptionAutocapture`, `enable_exception_autocapture`, `capture_panics`, +`errorTrackingConfig.autoCapture` — and register none of those handlers +yourself. A second listener on the same event sends every error twice, and it +stops matching the SDK's handling the moment the SDK changes. + +What the SDK cannot see is yours to add: errors a framework catches before any +global handler fires. Express error middleware, Fastify `setErrorHandler`, Hono +`onError`, Vue `app.config.errorHandler`, Angular `ErrorHandler`, SvelteKit +`handleError`, a React error boundary, Next.js `global-error` — hook those, +because the framework swallows the error and the global listener never hears +of it. A platform with no autocapture at all, such as Go, is the other case: +there the capture boundary at the entry point is the mechanism, not a copy of +one. + +## How you know you succeeded + +An error the app does not catch reaches PostHog, through the mechanism this +SDK gives you rather than one you invented, and no global error listener of +your own sits beside the SDK's autocapture. You did not install anything, run +a build, lint, or tests, search outside the project, or read through the whole +app or hand-wrap individual components or routes. Your handoff names the files +you changed and the capture mechanism, so the report can explain it to the +user. diff --git a/context/agents/error-tracking/configure.md b/context/agents/error-tracking/configure.md new file mode 100644 index 00000000..6c435550 --- /dev/null +++ b/context/agents/error-tracking/configure.md @@ -0,0 +1,113 @@ +--- +type: configure +flow: error-tracking +label: Apply build-config changes +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, check_env_keys] +disallowedTools: [enqueue_task] +dependsOn: [capture-exceptions] +--- + +## Goal + +Make this project's production build emit and upload source maps (or, for Go +and Rust, native debug symbols). Install the skill your task input names +(`install_skill` with the `skillId`) and read it — it is the source of truth +for the per-framework build-config and the uploader wiring. + +Two of the skill's steps are yours: + +- **"Apply build-config changes"** — make the bundler / build-config edits the + skill instructs for this platform, so the build produces and injects the + chunk IDs PostHog needs and runs the uploader. +- **"Make credentials available at build time"** — do the skill's step so the + build can read the upload credentials from the environment. If it calls for a + loader (e.g. `dotenv`), install it SILENTLY with the project's package + manager. Skip this step entirely when the platform already auto-loads `.env`. + +Install every dependency with the project's own package manager: call +`detect_package_manager` before the first install and use its answer, +translating any `npm install` the skill or docs show (`pnpm add -D …`, +`yarn add -D …`). In a pnpm or yarn workspace, npm fails outright on +`workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) — that error means the +wrong manager, never a flag to retry with. + +When the upload step you wire runs a machine-global `posthog-cli` rather than +one from the project's own dependencies, check that it is on the `PATH` +(`command -v posthog-cli`). Do not try to install it: a global install is +blocked for you, and the wizard already tried before your task. When it is +missing, name it in your handoff as a manual follow-up — run +`npm install -g @posthog/cli@latest` before the next release build — because +without it the upload step cannot run. + +When your changes make the build emit a bundle to a new directory (`dist/`, +`build/`), check that some script actually runs that output. A project whose +`build` writes `dist/index.js` while `start` still runs the original source +never executes the bundle the maps were uploaded for, so every uploaded map +goes unused. Add or fix the script that serves the built output. + +That run script has its own environment to satisfy, and it is not the build's. +The uploader's credentials are handled — your build step passes them itself. The +separate question is the variables **the application reads when it starts**: the +project token and host its init looks up. Those live in the same gitignored env +file, and a compiled binary or a bare interpreter loads nothing on its own. + +So open the init point, note the variables it reads, and make the run script +provide exactly those — export the env file ahead of the command, pass the +runtime's own flag for it, whatever that platform offers. Skip it and the +artifact starts, prints its own "variable missing" guard, and reports nothing: +the build is green, the symbols are uploaded, and the single command you hand +the user to verify with is the one command that cannot capture. + +Put a multi-step build somewhere the tool actually runs it — a script file, a +make target, the manifest's own scripts — never an alias mechanism you are +assuming exists, borrowed from a neighbouring tool. + +Match the bundle's module format to the package's type while you write the +command — you cannot run it, so it has to be right by construction. `esbuild +--platform=node` emits CommonJS unless you pass `--format=esm`, so in a package +whose `package.json` sets `"type": "module"` the bundle dies at boot with +`ReferenceError: module is not defined in ES module scope`. Read the `type` +field before writing the command. + +Types are the same kind of trap. In a config the compiler checks — a `.ts` +config, or JS under `checkJs` — `process.env.ANYTHING` is `string | undefined`, +while a plugin's options usually require `string`. Where your skill's example +asserts or defaults that lookup, keep that part exactly: dropping a `!` or a +`?? ''` turns a working example into a build that fails type checking. The +upload step often runs before the type check, so the maps land and the build +still exits non-zero — a broken build that looks half-successful in the log. + +## The names are a contract + +An env variable only works if the name the code reads is the name in the file. +You write the code that reads them; the `credentials` task writes the file, and +you two run in parallel. So never invent a name that already exists somewhere +else — look for the other half of the contract first, and adopt it: + +- **Call `check_env_keys` before you write the config.** It returns names, never + values. If the PostHog upload variables are already there, make your config + read exactly those names, whatever they are, even when they are not the ones + your skill's example shows. +- **Only when they are absent yet** are you the one deciding. Use the names your + skill documents for the mechanism you are wiring, and name them in your + handoff in full so `credentials` and `wire-ci` can match them. + +Names that merely look plausible are the failure here. A build reading +`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no +error anywhere — the upload is skipped silently, the build looks clean, and +every stack trace stays minified. + +Do not write any credential values and do not create env files — the +`credentials` task owns that, in parallel with you. Do not run the build. + +## How you know you succeeded + +The build config carries the skill's source-map / debug-symbol changes and can +read its credentials from the environment at build time. Your handoff names +every file you changed and the exact build-config keys you added, so the CI +task can wire the same variables through the pipeline. diff --git a/context/agents/error-tracking/credentials.md b/context/agents/error-tracking/credentials.md new file mode 100644 index 00000000..b09ebb63 --- /dev/null +++ b/context/agents/error-tracking/credentials.md @@ -0,0 +1,80 @@ +--- +type: credentials +flow: error-tracking +label: Get and write the upload credentials +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Put the PostHog source-map upload credentials into this project's environment. +Install the skill your task input names (`install_skill` with the `skillId`) +and follow its **"Write credentials to the env file"** step for the variable +names and the env file to pick. + +The upload needs a PostHog **personal API key** at build time. Only the user can +mint one — never call the PostHog API or any tool to create it. Get it and +write it in this one task (the key never survives across tasks): + +1. Ask with `wizard_ask`, exactly: + `{ id: "api-key", prompt: "Paste your PostHog personal API key below.\n\nDon't have one yet? Create one here:\n\n\nWhen creating the key, choose the 'Source map upload' preset, then come back and paste it here.", kind: "text", sensitive: true }` + You receive `{ secretRef: "secret:..." }` — a vaulted reference, never the raw + value. If `wizard_ask` is unavailable (non-interactive run), report this task + with status `not needed` and say in your handoff that the user must create + the key and set the variables themselves; do not block. +2. Pick the env file per the skill (reuse the one PostHog's SDK already writes + its `POSTHOG_*` vars to, when there is one). Call `check_env_keys` on it + first (it returns present/absent, never values — never read the file + directly). +3. Settle the variable names before you write them — see "The names are a + contract" below. Then call `set_env_values`, passing the secretRef as a + value object, not a literal string — e.g. + `values: { "POSTHOG_CLI_API_KEY": { secretRef: "" }, "POSTHOG_CLI_PROJECT_ID": "", "POSTHOG_CLI_HOST": "" }` + with whichever names you settled on. The wizard resolves the ref locally, so + you never see the key value. +4. Document the same variable names for other developers: append them to + `.env.example` (create it if the project has none) with empty or + placeholder values — never a real value, and never the key itself. The + example file is committed and is the only `.env*` you may write directly; + it is how the next developer, and the next wizard run's `check_env_keys`, + learns the project expects these variables. + +Replace ``, ``, and `` from your project +context. Do not touch the build config — the `configure` task owns that. + +## The names are a contract + +An env variable only works if the name in the file is the name the code reads. +You write the file; the `configure` task writes the code that reads it, and you +two run in parallel. So never invent a name that already exists somewhere else +— look for the other half of the contract first, and adopt it: + +- **Read the build config before you choose.** If it already references PostHog + env variables, use exactly those names, whatever they are, even when they are + not the ones your skill would suggest. Reading config is safe — it holds + names, not secrets. +- **Only when nothing references them yet** are you the one deciding. Use the + names your skill documents for the mechanism that was actually wired. +- **Either way, say which set you chose in your handoff, in full.** That is how + `configure` and `wire-ci` learn what to match. A handoff that says "the usual + variables" hands the next task the same guess you just made. + +Names that merely look plausible are the failure here. A build reading +`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no +error anywhere — the upload simply never runs, and every stack trace stays +minified. + +## How you know you succeeded + +The env file holds the upload variables (the key as a resolved secret, the +non-secret project id and host as literals), written through the wizard tools, +never hardcoded in source, and `.env.example` documents the same names with +placeholders. Your handoff names the env file and every variable name — never +a value — so the CI task carries the same names into the pipeline. diff --git a/context/agents/error-tracking/init.md b/context/agents/error-tracking/init.md new file mode 100644 index 00000000..d3059ba7 --- /dev/null +++ b/context/agents/error-tracking/init.md @@ -0,0 +1,117 @@ +--- +type: init +flow: error-tracking +label: Set up PostHog initialization +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [integration-v2-init, posthog-best-practices] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, set_env_values] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure PostHog is initialized. If the project already has a working +`posthog.init(...)` (or the framework's equivalent) with its env keys wired, +leave it alone and say so in your handoff. If it doesn't, create it following +your skill — it owns the how: the framework's init point, the env-var wiring +through the wizard tools, and `.env.example`. + +You exist in this flow because the user asked for error tracking on a repo +whose PostHog init is missing or unproven. Initialize the SDK so exceptions can +flow and stop — no instrumentation, no extras. Don't set up exception capture +either way; the capture-exceptions task after you owns that. + +## An existing init still needs its variable defined + +"Already initialised" is a property of the pair, not of the call. An init point +that reads `process.env.SOMETHING` only works when `SOMETHING` is defined in the +env file the project loads. A repo can carry an init that has never once run: +the call is there, the variable it names is nowhere, and the client is built +from an empty string. + +So before you leave an existing init alone, call `check_env_keys` on that env +file and look for the exact name the init reads. It returns names, never values. + +- **Present** — the pair is complete. Leave it alone and say so. +- **Absent** — the init is not wired yet, whoever wrote it. Write that variable + with `set_env_values` under the name the code already reads, and document it + in `.env.example`. Do not rename the code to match a name you would rather + have written; the code is the half that already exists. + +An empty key is the quiet failure here. A client constructed from `''` throws +nothing and logs nothing. The build is clean, the app starts, every capture call +returns — and no event ever arrives. + +## Make the environment actually reachable + +Writing the keys to `.env` is only half the job — something has to load that +file at runtime, or the app throws on boot and captures nothing. + +Most frameworks do it for you: Next, Nuxt, Astro and SvelteKit auto-load `.env`, +and Vite auto-loads it for client code. Nothing to do there. + +A plain Node backend does not — Express, Fastify, Hono, Koa, a raw `node:http` +server — and neither does a bare Rollup or webpack config. When your init point +reads `process.env` on one of those, wire the loading too, either way: + +- install `dotenv` with the project's own package manager (detect it from the + lockfile) and import it above the PostHog init — `require('dotenv').config()`, + or `import 'dotenv/config'` for ESM; or +- add `--env-file=.env` to the `start` and `dev` scripts, when the project is on + Node 20.6+ and would rather not take a new dependency. + +You cannot run the app, so this has to be right by construction. Writing the +guard without the loader is what produces +`POSTHOG_… variable required by PostHog is missing or un-configured` at module +load: the guard you wrote firing against an env file nothing reads. + +Some platforms have no environment to read at all, and there the answer is not +a loader. Angular on the stock `@angular/build` builder is the common one: +nothing populates `process.env`, `import.meta.env`, or the project's own +`src/environments/.env.ts` with your keys. + +The trap is that each of those *looks* like a mechanism. `import.meta.env` and +the `NG_APP_` prefix only exist with `@ngx-env/builder` installed, and a +generated `.env.ts` usually carries one unrelated key such as +`npm_package_version` and nothing else. Read from either and your key is +`undefined`: in development the guard throws while the module evaluates and the +app renders a blank page, and in a production build the guard returns quietly, +so the app looks fine while PostHog never initialises at all. + +So do not wire a lookup unless you have opened the thing it reads from and seen +your key defined there. When nothing populates it, write the real public project +token as a literal in the committed `src/environments/*` files. This is the +skill's "no valid environment to read from" case, and the public token is +publishable — it ships inside the browser bundle either way. + +## The init has to type-check + +You cannot run the build either. So in a project the compiler checks — +TypeScript, or JavaScript under `checkJs` — the init has to compile by +construction. `process.env.ANYTHING` is `string | undefined`, and the SDK +constructor wants a `string`. + +Pass the variable that you checked. `if (!key) { … } else { new PostHog(key, …) }` +narrows `key` to `string`. A check on a different variable does not: store the +missing name in `missing`, test `missing`, then pass `key`, and the compiler +still sees `string | undefined`. The build then stops with "Argument of type +'string | undefined' is not assignable to parameter of type 'string'", emits +no bundle, and uploads no source maps. When the existing code already passes +`process.env.KEY ?? ''` or `process.env.KEY!`, keep that part as it is. + +## How you know you succeeded + +An init point exists with the PostHog env keys present — whether it already +did or you just created it — keys in the env file and confirmed there with +`check_env_keys`, never hardcoded. On a platform that does not auto-load +`.env`, the loading is wired; on a platform with no environment at all, the +token is a literal rather than a lookup into something that never defines it. +In a type-checked project, the value you pass to the SDK is narrowed to a +`string`. +Your handoff names the files involved, how the client is constructed, and how +`.env` reaches it, so the capture-exceptions task can find the init options +without re-discovering them. diff --git a/context/agents/error-tracking/install.md b/context/agents/error-tracking/install.md new file mode 100644 index 00000000..8ddb6527 --- /dev/null +++ b/context/agents/error-tracking/install.md @@ -0,0 +1,31 @@ +--- +type: install +flow: error-tracking +label: Add the PostHog SDK to the manifest +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-haiku-4-5 +skills: [integration-v2-install] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Make sure the PostHog SDK is in the manifest. If it's already installed, +leave it alone and say so in your handoff. If it isn't, install it following +your skill — it owns the how: the package manager rules, the version rules, +what counts as an environment failure, and the fallback. + +You only exist in this flow because the user asked for error tracking on a +repo without PostHog. Install the SDK the errors will report through (the +server library too, if the app runs server-side code) and stop — no +instrumentation, no extras. + +## How you know you succeeded + +The SDK is declared in the manifest at a real version — whether it already +was or you just installed it — or your handoff plainly says why the +environment stopped you. Your handoff names the manifest and the package, so +later steps import it under the name they will actually get. diff --git a/context/agents/error-tracking/report.md b/context/agents/error-tracking/report.md new file mode 100644 index 00000000..83fb18f2 --- /dev/null +++ b/context/agents/error-tracking/report.md @@ -0,0 +1,59 @@ +--- +type: report +flow: error-tracking +label: Summarise and hand off +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-sonnet-5 +effort_sdk: medium +skills: [] +allowedTools: [Read, Glob, Grep, Write] +disallowedTools: [enqueue_task] +dependsOn: [capture-exceptions, wire-ci] +--- + +## Goal + +Tell the user what error tracking now does for them and what they still have +to do, from the handoffs of every task in the run. `read_handoffs` gives you +each task's report — the capture mechanism, the files changed, the env +variable names, the CI secret to create, and any deploy path that could not be +traced. Do not re-derive any of it from the project. + +Write the hand-off to `posthog-error-tracking-report.md` at the top level of +this project's directory. When the run wired source-map upload, START it with +a **"What you still need to do"** section — numbered, copy-pasteable: + +1. Create a personal API key with the 'Source map upload' preset at + `/settings/user-api-keys` (skip when the credentials handoff says + the key is already written). +2. Add it as the CI secret the wire-ci step referenced, named exactly as in + the pipeline config. +3. Any other manual follow-up the handoffs carry (an untraceable deploy path, + provider-side settings, installing a missing `posthog-cli`). + +Then cover, briefly and concretely: + +- How uncaught errors reach PostHog now — the capture mechanism and the files + that carry it. +- If the run also installed and initialized the SDK, say so — the user + started this command without PostHog and now has it. +- When source-map upload was wired: the files changed (paths only), the exact + production build command, and that every production build now uploads. +- When it was skipped: one line saying why — readable stack traces on this + platform, or that Astro is not supported by the uploader. An outcome, not an + apology. Say plainly that the build command was left untouched. +- How to verify: trigger any error and look at + `/project//error_tracking`; uploaded symbol sets appear + at `/project//error_tracking/configuration`. + +Never write a secret value into the report — only variable names. Replace +`` and `` from your project context. Give the same +summary in chat. + +## How you know you succeeded + +`posthog-error-tracking-report.md` exists and a user who reads only it knows +how errors reach PostHog, the follow-ups they still owe (the API key and the +CI secret named exactly, when upload was wired), and where in PostHog to see +the first captured exception. diff --git a/context/agents/error-tracking/setup-error-tracking.md b/context/agents/error-tracking/setup-error-tracking.md new file mode 100644 index 00000000..bfce880d --- /dev/null +++ b/context/agents/error-tracking/setup-error-tracking.md @@ -0,0 +1,119 @@ +--- +type: setup-error-tracking +flow: error-tracking +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Glob, Grep, posthog_exec] +disallowedTools: [Write, Edit, Bash, complete_task] +dependsOn: [] +--- + +## Goal + +Plan a PostHog Error Tracking setup and seed the task queue. The end state: +errors the app does not catch reach PostHog, and — where the platform ships +minified bundles or stripped binaries — production builds upload the source +maps or debug symbols that make the stack traces readable. + +First establish two facts from the repo: + +**1. Is PostHog already integrated?** Look for `posthog-js` or a server SDK in +the dependency manifests, or a `posthog.init(...)` / snippet in the source. +Check the project state for existing events if the repo is ambiguous. + +Integrated means the init runs, not that the package is listed. An init point +is a pair: the call, and the key it is constructed from. A repo can carry the +call while the key it names is defined nowhere — then the client is built from +an empty string and captures nothing, however complete the manifest looks. So +when you find an init that reads a variable, look for that same name in the +repo's committed env template or its build config. Found, or the project state +shows real events arriving: integrated. Named nowhere, or you cannot tell: +**queue `init`**. It re-checks the pair itself and leaves a complete init +alone, so queuing it when you are unsure costs one cheap task, while skipping +it on a keyless init costs the whole run. + +**2. Which uploader variant is this project — or none?** Read the manifests +and pick at most one, by this precedence (first match wins): + +- `react-native` or `expo` in `package.json` dependencies → `react-native`. + A React Native repo also carries `ios/Podfile`, an `.xcodeproj` and Gradle + files for its native shells, so this rule wins over every native marker + below. Picking `ios` or `android` for it uploads no JavaScript source maps, + and every JavaScript stack trace stays minified. +- `pubspec.yaml` → `flutter` +- an `.xcodeproj`, `Podfile`, or `Package.swift` → `ios` +- a Gradle build file that applies `com.android.application` or + `com.android.library`, or an `AndroidManifest.xml` → `android`. A Gradle + project without these markers is a JVM server: keep reading. +- `go.mod` → `go` +- `Cargo.toml` → `rust` +- `astro` in `package.json` dependencies → **none**. Astro is not supported by + the uploader: it inlines scripts below its asset limit into the HTML, so a + build routinely emits a `.map` with no `.js` beside it, and the upload step + then fails the whole build. This rule wins over every `package.json` match + below — an Astro project that also depends on `vite` is still **none**. +- otherwise read `package.json` dependencies, first match wins: + `nuxt` → `nuxt`; `next` → `nextjs`; + `@angular/core` → `angular`; `vite` → `vite`; `webpack` → `webpack`; + `rollup` → `rollup`; `react` → `react`; server-only Node → `node`; + any other browser JS → `web` +- **none** for platforms whose stack traces are already readable: plain + Python (Django, Flask, FastAPI), Ruby, PHP, Elixir, JVM servers, .NET. + Skip the whole upload subgraph for them — a skipped upload on such a + platform is an outcome, not a gap. + +When a variant matched, the uploader skill id is +`error-tracking-upload-source-maps-`. Pass it to the four upload +tasks as `inputs: { skillId: "", displayName: "" }` +so no task re-detects. + +The two facts are independent — settle BOTH before you enqueue anything. +"PostHog is already integrated" answers fact 1 only; it never decides fact 2, +and an already-integrated project still gets the upload subgraph when a +variant matches. A compiled or bundled JS project normally has one: a Node +service built with `tsc` ships minified/compiled output, so it is the `node` +variant, not "none". Only two kinds of project skip the subgraph — the +readable-stack platforms listed above, and Astro. + +Then seed the graph: + +- `install`, only when the SDK is missing from the manifest. +- `init`, independent of `install` — whenever fact 1 did not show a complete + pair. Do not stop on an uninstrumented repo, integrate. +- `capture-exceptions`, after whichever of `install` and `init` you queued + (with no dependencies when you queued neither). +- When an uploader variant matched, add the upload subgraph: + - `credentials`, no dependencies — it stops to ask the user for a personal + API key, so keep it a root task: the prompt reaches the user early while + the code tasks run. + - `configure`, after `capture-exceptions` — build-config changes; it runs + after the code edits so the two never fight over the same files. + - `wire-ci`, after `configure` and `credentials`. +- `report`, after every other queued task except `test-setup`. It writes the + handoff once the work is done, so it describes what actually shipped. +- When you queued the upload subgraph, `test-setup` last, after `report` — it + offers the user an optional local end-to-end test. The report comes first on + purpose: a user who walks away still gets it. Queue `report` before + `test-setup`, because `test-setup` names `report` as its dependency. + +Never plan an identify, capture, dashboard, or session-replay task — this run +sets up error tracking, not the full integration. The minimal SDK footprint +that `install` and `init` leave behind is enough for exceptions to flow. + +## How you know you succeeded + +Every task in the chosen graph is queued with that dependency shape, the four +upload tasks (when queued) share the same `{ skillId, displayName }` inputs, +`report` depends on every other task except `test-setup` (directly or +transitively), `test-setup` (when queued) depends on `report`, and the first +task is runnable. Your plan states both facts explicitly: whether PostHog was +integrated — and, when you called it integrated, the name of the key you found +defined — and which uploader variant matched — or, when you queue no upload +tasks, why no variant applies: which readable-stack platform this is, or that +Astro is not supported by the uploader. A +plan that never mentions fact 2 is an incomplete plan, not a decision. Keep +labels short — the action in a few words. diff --git a/context/agents/error-tracking/test-setup.md b/context/agents/error-tracking/test-setup.md new file mode 100644 index 00000000..505f434a --- /dev/null +++ b/context/agents/error-tracking/test-setup.md @@ -0,0 +1,52 @@ +--- +type: test-setup +flow: error-tracking +label: Offer an optional local test +sink: true +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask] +disallowedTools: [enqueue_task] +dependsOn: [report] +--- + +## Goal + +The setup is done and the report is written. Offer the user an optional, +one-time, end-to-end check that errors reach PostHog with readable stack traces. +Install the skill your task input names (`install_skill` with the `skillId`) and +follow its **"Test the local setup"** step for the platform-appropriate +affordance, the `captureException` shape, the placement, and the +read-before-edit / always-revert rules. + +You run after the report on purpose. A user who stepped away still gets a +finished setup and a report, so nothing waits on this test. + +First ask with `wizard_ask`: +`{ id: "test-affordance", prompt: "Error tracking is set up, and the report is in posthog-error-tracking-report.md. Optional: want me to help you test it locally? I'll add a temporary test button (or route) so you can confirm errors show up in Error Tracking with readable stack traces after your next build, then remove it.", kind: "single", options: [{ label: "Yes, help me test it", value: "yes" }, { label: "No, I'm done", value: "no" }] }` + +- **"no"**, no answer (`__cancelled__`), or `wizard_ask` unavailable: do nothing + to the code and report this task done, noting the test was offered and not + taken. +- **"yes"**: add the affordance per the skill, then pause with a second + `wizard_ask` (id `"test-done"`, a single `Continue (revert test code)` + option) whose prompt gives the build, run, and Error-Tracking-check as + literal numbered steps (build first — it uploads the maps — then trigger the + affordance, then confirm the error resolves to real source in Error + Tracking). After the user continues, or when that ask comes back unanswered, + REVERT every test edit per the skill's rules. Never leave the affordance in + place, even if the user says it didn't work. + +When the user says the test failed, add a short **"Local test"** section at the +end of `posthog-error-tracking-report.md` with what they saw, and say the same +in chat. The report task has already run, so this is the only place the +failure is written down. + +## How you know you succeeded + +Either the offer was declined or unanswered and no code changed, or the +affordance was added, tested, and fully reverted. Your handoff says which, and +a failure the user reported is in the report file. diff --git a/context/agents/error-tracking/wire-ci.md b/context/agents/error-tracking/wire-ci.md new file mode 100644 index 00000000..958a6fe6 --- /dev/null +++ b/context/agents/error-tracking/wire-ci.md @@ -0,0 +1,89 @@ +--- +type: wire-ci +flow: error-tracking +label: Set up CI for automatic uploads +model_pi: openai/gpt-5.6-sol +effort_pi: medium +model_sdk: claude-sonnet-5 +effort_sdk: high +skills: [] +allowedTools: [Read, Write, Edit, Glob, Grep, load_skill_menu, install_skill] +disallowedTools: [enqueue_task] +dependsOn: [configure, credentials] +--- + +## Goal + +Make the credentials reach the production build wherever it actually runs, so +source maps upload on every deploy — not just on a local build. Install the +skill your task input names (`install_skill` with the `skillId`) and follow its +**"Set up CI for automatic uploads"** step — it owns tracing where the +production build runs and wiring the credentials through every layer, whatever +the CI provider. + +The `configure` and `credentials` handoffs already name the build-config keys +and the exact environment-variable names in use — carry those same names into +the pipeline; do not invent new ones. Trace the deploy path by reading the +project's own files (CI workflows, Dockerfiles, deploy scripts) — never invent +config that is not there. You cannot create the CI secret that holds the API +key; reference it by name and carry that follow-up, plus any deploy path you +could not trace, into your handoff for the report. + +## Two things cross the boundary, not one + +Credentials are half of it. The uploader also needs a **release identity** — a +name and a version — and it derives that from the CI's own git variables or +from a `.git` directory. A container build sees neither: `.git` is almost +always in `.dockerignore`, and the CI's variables stop at the `docker build` +command. Nor does a hardcoded release name rescue you; the uploader wants both +halves, and stops the build when it has only one. + +So forward the provider's git variables into the build the same way you forward +credentials, and declare each one as `ARG` **and** `ENV` — `ARG` alone is not +visible to the uploader's environment lookup. Your skill's "Associate the +release with a git commit" step lists the variables per provider. Read that +step even though it is not the CI step: this boundary is where it applies. + +Forward them only where a provider actually sets them. A project with no +pipeline — an image built and run by hand — has nothing to inherit from, and +variables declared but never filled resolve to no release at all: it reads as +wired and still fails the build. There the identity has to be supplied outright, +both halves, from something the build itself holds — a build argument the +operator passes, or the manifest's own version — so name and version are always +present. Decide which case you are in by reading the repo, not by assuming a +provider exists. + +That `ARG`-plus-`ENV` shape is for the git variables and the non-secret settings +only. The API key is a secret and keeps whatever secret-carrying mechanism the +build system offers — a build secret mounted for the one step that needs it, a +masked variable, a secret file. A secret in `ARG` or `ENV` is recorded in the +build history, and the builder itself will warn you: *do not use ARG or ENV +instructions for sensitive data*. Widening the git-variable pattern to cover the +key is a downgrade, not consistency. + +## The runtime boundary has names too + +Upload credentials are a build-time concern. The app also reads its own +variables at **run** time, and the step that starts it is another place a name +has to match — a `docker run -e`, a compose file, a systemd unit, a platform's +environment settings. That step was written before PostHog existed here, so the +name it passes is whatever the project used back then. If `init` settled on a +different one, the deployed process starts with an undefined key. + +Nothing fails loudly. In production the init guard returns quietly instead of +throwing, so the process boots, serves traffic, and reports nothing — the same +silent shape as a mismatched upload variable, one boundary later. + +So read the names the app's own source reads, and make the start step pass +exactly those. Rename the deploy step's variable, not the code: the code is the +half that already exists. Name any secret the user must create under its new +name in your handoff. + +## How you know you succeeded + +The pipeline that runs the production build carries the upload credentials by +the same names the credentials task used and reaches the uploader with a +resolvable release identity, and the step that starts the app passes the +variable names the app's own source reads. Every secret the user still has to create is named +in your handoff. Your handoff lists the CI files you changed +and every manual follow-up, so the report can hand them to the user. diff --git a/context/skills/error-tracking-upload-source-maps/description.md b/context/skills/error-tracking-upload-source-maps/description.md index bd355029..37249760 100644 --- a/context/skills/error-tracking-upload-source-maps/description.md +++ b/context/skills/error-tracking-upload-source-maps/description.md @@ -34,10 +34,11 @@ Wire source map generation, chunk-ID injection, and upload into your **productio - **Don't ship source maps publicly**: omit `.map` files 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. - **Never write a dependency version from memory.** When a variant needs the CLI or a plugin *inside the project* — the JS/web build tools (Node, web, Next.js, React, Angular, Nuxt, Vite, Webpack, Rollup) — install it with the project's package manager pinned to `latest` (`npm install --save-dev @posthog/cli@latest`, `pnpm add -D @posthog/cli@latest`, `yarn add -D @posthog/cli@latest`) and let the manager write the resolved version into `package.json`. A version recalled from memory is usually far behind and silently breaks the commands in this skill — `--dotenv-file`, for one, is rejected outright by 0.5.x. iOS, Android, React Native, Flutter, Go and Rust are the exception: the wizard pre-installs a global `posthog-cli` for those, so add no project dependency for them at all. +- **Match the project's package manager, not the docs'.** Example commands in the docs install with `npm`. Before any install, detect the real manager from the lockfile — `pnpm-lock.yaml` → `pnpm add -D`, `yarn.lock` → `yarn add -D`, `package-lock.json` / none → `npm install --save-dev` (the wizard's `detect_package_manager` tool answers this) — and translate the command. This is load-bearing in monorepos: npm hard-fails on `workspace:*` dependencies with `EUNSUPPORTEDPROTOCOL`, so an npm install copied into a pnpm workspace cannot succeed, and no npm flag fixes it — switch the command to the detected manager instead of retrying. #### Examples - **Node / tsc** Emit maps with embedded sources by setting both in `tsconfig.json`: `"sourceMap": true` and `"inlineSources": true`. Add the CLI to the project with `npm install --save-dev @posthog/cli@latest` (or the project's package manager) so the build script and CI resolve the same binary — never hand-write the version string. 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. +- **Vite / Webpack / Rollup** Prefer the bundler plugin from the reference over hand-rolling the CLI — it injects and uploads in one pass. For Vite and Rollup, make sure the bundler is configured to emit source maps. **Webpack is the exception:** with `sourcemaps.enabled`, the PostHog webpack plugin adds its own `SourceMapDevToolPlugin`, so do not also set a `devtool` that emits maps. Remove `devtool: 'source-map'` or `'hidden-source-map'`, or set `devtool: false`. When both emit `[file].map`, the build fails with `Conflict: Multiple assets emit different content to the same filename index.js.map` and nothing uploads. - **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`. diff --git a/context/skills/integration-v2/error-tracking-step/config.yaml b/context/skills/integration-v2/error-tracking-step/config.yaml index 89c15127..cad9c778 100644 --- a/context/skills/integration-v2/error-tracking-step/config.yaml +++ b/context/skills/integration-v2/error-tracking-step/config.yaml @@ -1,10 +1,433 @@ # Orchestrator step-skill: fetched by task agents, never surfaced as a command. -# The framework matrix is borrowed from the canonical integration group. +# The framework matrix mirrors the canonical integration group's ids, but the +# docs are its own: integration teaches SDK setup, this step teaches exception +# capture, and for several platforms those are different pages. Borrowing the +# matrix would hand the capture task the library page and nothing about panic +# hooks, log handlers, or framework error boundaries. type: docs-only template: description.md description: Capture exceptions with PostHog around critical flows tags: [orchestrator, error-tracking] cli: role: internal -variants_from: integration +variants: +- id: nextjs-app-router + framework: nextjs + default: true + display_name: Next.js App Router + tags: + - nextjs + - react + - ssr + - app-router + - javascript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/nextjs.md +- id: nextjs-pages-router + framework: nextjs + display_name: Next.js Pages Router + tags: + - nextjs + - react + - ssr + - pages-router + - javascript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/nextjs.md +- id: react-react-router-6 + framework: react-router + default: true + display_name: React Router v6 + tags: + - react + - react-router + - v6 + - spa + - javascript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md +- id: react-react-router-7-framework + framework: react-router + display_name: React Router v7 - Framework mode + tags: + - react + - react-router + - v7 + - framework + - ssr + - javascript + - javascript_node + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md +- id: react-react-router-7-data + framework: react-router + display_name: React Router v7 - Data mode + tags: + - react + - react-router + - v7 + - data + - spa + - javascript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md +- id: react-react-router-7-declarative + framework: react-router + display_name: React Router v7 - Declarative mode + tags: + - react + - react-router + - v7 + - declarative + - spa + - javascript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md +- id: react-vite + display_name: React (Vite) + tags: + - react + - vite + - spa + - javascript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md +- id: nuxt-3-6 + framework: nuxt + display_name: Nuxt 3.6 + tags: + - nuxt + - javascript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/nuxt-3-6.md +- id: nuxt-4 + framework: nuxt + default: true + display_name: Nuxt 4 + tags: + - nuxt + - vue + - ssr + - javascript + - typescript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/nuxt-3-7.md +- id: vue-3 + framework: vue + display_name: Vue 3 + tags: + - vue + - javascript + - typescript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/web.md + - https://posthog.com/docs/libraries/vue-js.md +- id: django + framework: django + display_name: Django + tags: + - django + - python + docs_urls: + - https://posthog.com/docs/error-tracking/installation/python.md + - https://posthog.com/docs/libraries/django.md +- id: flask + framework: flask + display_name: Flask + tags: + - flask + - python + docs_urls: + - https://posthog.com/docs/error-tracking/installation/python.md + - https://posthog.com/docs/libraries/flask.md +- id: fastapi + framework: fastapi + display_name: FastAPI + tags: + - fastapi + - python + docs_urls: + - https://posthog.com/docs/error-tracking/installation/python.md + - https://posthog.com/docs/libraries/python.md +- id: react-tanstack-router-file-based + framework: tanstack-router + display_name: React with TanStack Router (file-based) + tags: + - react + - tanstack-router + - spa + - javascript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md + - https://posthog.com/docs/libraries/tanstack-start.md +- id: react-tanstack-router-code-based + framework: tanstack-router + default: true + display_name: React with TanStack Router (code-based) + tags: + - react + - tanstack-router + - spa + - javascript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md + - https://posthog.com/docs/libraries/tanstack-start.md +- id: tanstack-start + framework: tanstack-start + display_name: TanStack Start + tags: + - react + - tanstack-start + - tanstack-router + - javascript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react.md + - https://posthog.com/docs/libraries/tanstack-start.md +- id: laravel + framework: laravel + display_name: Laravel + tags: + - laravel + - php + docs_urls: + - https://posthog.com/docs/error-tracking/installation/php.md + - https://posthog.com/docs/libraries/laravel.md +- id: php + display_name: PHP + tags: + - php + docs_urls: + - https://posthog.com/docs/error-tracking/installation/php.md +- id: wordpress + framework: wordpress + display_name: WordPress + tags: + - wordpress + - php + - cms + docs_urls: + - https://posthog.com/docs/error-tracking/installation/php.md + - https://posthog.com/docs/libraries/wordpress.md + - https://posthog.com/docs/libraries/php.md +- id: ruby-on-rails + framework: rails + display_name: Ruby on Rails + tags: + - ruby-on-rails + - ruby + docs_urls: + - https://posthog.com/docs/error-tracking/installation/ruby-on-rails.md +- id: android + framework: android + display_name: Android + tags: + - android + - java + - kotlin + docs_urls: + - https://posthog.com/docs/error-tracking/installation/android.md +- id: sveltekit + framework: sveltekit + display_name: SvelteKit + tags: + - sveltekit + - svelte + - javascript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/svelte.md + - https://posthog.com/docs/libraries/svelte.md +- id: python + framework: python + display_name: Python + tags: + - python + docs_urls: + - https://posthog.com/docs/error-tracking/installation/python.md +- id: javascript_node + framework: javascript_node + display_name: JavaScript Node + tags: + - javascript_node + - javascript + docs_urls: + - https://posthog.com/docs/error-tracking/installation/node.md +- id: javascript_web + framework: javascript_web + display_name: JavaScript Web + tags: + - javascript_web + - javascript + docs_urls: + - https://posthog.com/docs/error-tracking/installation/web.md +- id: ruby + framework: ruby + display_name: Ruby + tags: + - ruby + docs_urls: + - https://posthog.com/docs/error-tracking/installation/ruby.md +- id: elixir + framework: elixir + display_name: Elixir + tags: + - elixir + - phoenix + - plug + docs_urls: + - https://posthog.com/docs/error-tracking/installation/elixir.md +- id: java + framework: java + display_name: Java (Spring Boot) + tags: + - java + - java-server + docs_urls: + - https://posthog.com/docs/libraries/java.md +- id: go + framework: go + display_name: Go + tags: + - go + docs_urls: + - https://posthog.com/docs/error-tracking/installation/go.md +- id: rust + framework: rust + display_name: Rust + tags: + - rust + docs_urls: + - https://posthog.com/docs/error-tracking/installation/rust.md +- id: swift + framework: swift + display_name: Swift (iOS/macOS) + tags: + - swift + - ios + - macos + - swiftui + - xcodegen + docs_urls: + - https://posthog.com/docs/error-tracking/installation/ios.md + - https://posthog.com/docs/libraries/ios.md + - https://posthog.com/docs/libraries/ios/usage.md + - https://posthog.com/docs/libraries/ios/configuration.md +- id: flutter + framework: flutter + display_name: Flutter + tags: + - flutter + - dart + - mobile + docs_urls: + - https://posthog.com/docs/error-tracking/installation/flutter.md +- id: kmp + display_name: Kotlin Multiplatform + tags: + - kmp + - kotlin + - multiplatform + - mobile + docs_urls: + - https://posthog.com/docs/error-tracking/installation/kmp.md +- id: react-native + framework: react-native + default: true + display_name: React Native + tags: + - react-native + - javascript + - typescript + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react-native.md +- id: expo + framework: react-native + display_name: Expo + tags: + - expo + - react-native + - javascript + - typescript + docs_urls: + - https://posthog.com/docs/error-tracking/installation/react-native.md + - https://posthog.com/docs/libraries/react-native.md +- id: astro-static + framework: astro + display_name: Astro (Static) + tags: + - astro + - javascript + - typescript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/web.md + - https://posthog.com/docs/libraries/astro.md +- id: astro-view-transitions + framework: astro + display_name: Astro (View Transitions) + tags: + - astro + - astro-view-transitions + - javascript + - typescript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/web.md + - https://posthog.com/docs/libraries/astro.md +- id: astro-ssr + framework: astro + display_name: Astro (SSR) + tags: + - astro + - astro-ssr + - javascript + - typescript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/web.md + - https://posthog.com/docs/libraries/astro.md +- id: astro-hybrid + framework: astro + default: true + display_name: Astro (Hybrid) + tags: + - astro + - astro-hybrid + - javascript + - typescript + - javascript_web + - javascript_node + docs_urls: + - https://posthog.com/docs/error-tracking/installation/web.md + - https://posthog.com/docs/libraries/astro.md +- id: angular + framework: angular + display_name: Angular + tags: + - angular + - javascript + - typescript + - javascript_web + docs_urls: + - https://posthog.com/docs/error-tracking/installation/angular.md packaging: bundle diff --git a/scripts/lib/tests/error-tracking-step-parity.test.js b/scripts/lib/tests/error-tracking-step-parity.test.js new file mode 100644 index 00000000..8ec49a1b --- /dev/null +++ b/scripts/lib/tests/error-tracking-step-parity.test.js @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { join } from 'path'; + +import { loadSkillsConfig } from '../skill-generator.js'; + +const CONFIG_DIR = join(process.cwd(), 'context'); + +// error-tracking-step declares its variants literally (its docs differ from +// integration's), but the orchestrator resolves it per framework with no +// fallback. A variant added to integration and not copied here makes the +// default orchestrator flow abort for that framework. +describe('integration-v2/error-tracking-step', () => { + it('declares every integration variant, with the same framework', () => { + const config = loadSkillsConfig(CONFIG_DIR); + const keys = (group) => config[group].variants.map((v) => `${v.id}:${v.framework}`); + + expect(keys('integration-v2/error-tracking-step')).toEqual( + expect.arrayContaining(keys('integration')), + ); + }); +}); diff --git a/scripts/scan-warlock.js b/scripts/scan-warlock.js index 1015ff78..adddb94f 100644 --- a/scripts/scan-warlock.js +++ b/scripts/scan-warlock.js @@ -68,10 +68,11 @@ const isCI = Boolean(process.env.CI); // matches. The LLM decides whether each match is a real threat or a // false positive // -// Gateway URL pattern matches the wizard: -// US: https://gateway.us.posthog.com/wizard -// EU: https://gateway.eu.posthog.com/wizard -// Local: http://localhost:3308/wizard +// The gateway's `ci` product serves CI runs with a personal API key. The +// legacy `wizard` product is retired and refuses every caller with a 403. +// US: https://gateway.us.posthog.com/ci +// EU: https://gateway.eu.posthog.com/ci +// Local: http://localhost:3308/ci function getGatewayUrl() { const host = process.env.POSTHOG_HOST || "https://us.posthog.com"; @@ -88,12 +89,12 @@ function getGatewayUrl() { } if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") { - return "http://localhost:3308/wizard"; + return "http://localhost:3308/ci"; } if (hostname === "eu.posthog.com" || hostname === "eu.i.posthog.com") { - return "https://gateway.eu.posthog.com/wizard"; + return "https://gateway.eu.posthog.com/ci"; } - return "https://gateway.us.posthog.com/wizard"; + return "https://gateway.us.posthog.com/ci"; } function createLLMProvider() {