From fa48c045610718151cd0ddc834a9701eec16674e Mon Sep 17 00:00:00 2001 From: Marco Gancitano Date: Mon, 31 Aug 2026 12:40:03 -0400 Subject: [PATCH 1/2] feat(skills): route Go projects to the OpenTelemetry AIO variant Go has no PostHog wrapper SDK; PostHog/posthog-go#306 adds an OTel bridge instead. Add the opentelemetry-go variant and make a go.mod project take it first, before the framework and provider rules. Co-Authored-By: Claude Fable 5 --- context/skills/ai-observability/config.yaml | 8 ++++++++ context/skills/ai-observability/description.md | 4 ++-- .../ai-observability/references/1-begin.md | 16 ++++++++++------ .../ai-observability/references/2-install.md | 2 ++ .../ai-observability/references/3-instrument.md | 2 ++ 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/context/skills/ai-observability/config.yaml b/context/skills/ai-observability/config.yaml index 7ff6d2ad..10b582d5 100644 --- a/context/skills/ai-observability/config.yaml +++ b/context/skills/ai-observability/config.yaml @@ -571,6 +571,14 @@ variants: - https://posthog.com/docs/ai-observability/installation/opentelemetry.md - https://posthog.com/docs/libraries/node.md + - id: opentelemetry-go + framework: opentelemetry + display_name: OpenTelemetry (Go) + tags: [opentelemetry, go, ai_observability] + docs_urls: + - https://posthog.com/docs/ai-observability/installation/opentelemetry.md + - https://posthog.com/docs/libraries/go.md + - id: claude-code framework: claude-code display_name: Claude Code diff --git a/context/skills/ai-observability/description.md b/context/skills/ai-observability/description.md index c43efde0..3902de55 100644 --- a/context/skills/ai-observability/description.md +++ b/context/skills/ai-observability/description.md @@ -6,7 +6,7 @@ Wire up PostHog's AI Observability so calls made through {display_name} land in This skill instruments the LLM calls the project *already makes*. It does **not** install the vendor SDK for you. -Check the project's manifest for an LLM package. The catalog is far wider than the obvious providers — 68 variants covering agent frameworks (`openai-agents`, `claude-agent-sdk`, LangGraph, CrewAI, Mastra, …) and OpenAI-compatible gateways (Groq, OpenRouter, Together, Ollama, …), which an app reaches through the `openai` package plus a `baseURL` override. `1-begin.md` carries the ordered decision rules; follow them rather than matching on the first familiar package name. If no LLM SDK is present, switch to the `manual-capture` variant — it posts `$ai_generation` events directly and works standalone. +Check the project's manifest for an LLM package. The catalog is far wider than the obvious providers — 69 variants covering agent frameworks (`openai-agents`, `claude-agent-sdk`, LangGraph, CrewAI, Mastra, …) and OpenAI-compatible gateways (Groq, OpenRouter, Together, Ollama, …), which an app reaches through the `openai` package plus a `baseURL` override. `1-begin.md` carries the ordered decision rules; follow them rather than matching on the first familiar package name. If no LLM SDK is present, switch to the `manual-capture` variant — it posts `$ai_generation` events directly and works standalone. Everything else this skill needs — PostHog credentials, instrumentation packages, env vars — the skill installs and configures itself. It does **not** require a pre-existing `posthog.init(...)`. If one is already there, reuse its env-var names in `3-instrument.md`; if not, that step sets fresh values via `set_env_values`. @@ -28,7 +28,7 @@ The linked install page carries the exact code blocks for this variant's languag ## Key principles - **Environment variables.** Read `` and `` from env, using the framework's env-var convention. Never hardcode either value. -- **The SDK wrapper is the default, not OpenTelemetry.** OTel makes the session tree awkward to build and maintain, so provider and gateway variants use PostHog's drop-in wrapper client. Reserve OTel for the `opentelemetry-*` variants and LlamaIndex, and never swap a framework's own tracing hook for an instrumentor. +- **The SDK wrapper is the default, not OpenTelemetry.** OTel makes the session tree awkward to build and maintain, so provider and gateway variants use PostHog's drop-in wrapper client. Reserve OTel for the `opentelemetry-*` variants and LlamaIndex, and never swap a framework's own tracing hook for an instrumentor. Go is the exception: it has no wrapper SDK, so every Go project uses the `opentelemetry-go` bridge. - **Minimal changes.** The wrapper swaps a client constructor and adds parameters to existing calls. Don't restructure the app, and don't wrap the setup in an init function or module globals. - **Match the docs.** Package names and wrapper imports change between AIO releases. The install page for this variant is the source of truth. - **Cardinality is what gets graded.** One `$ai_session_id` per conversation, one `posthog_trace_id` per turn, shared by every call in it. An id minted per call is worse than none — it looks instrumented and groups nothing. diff --git a/context/skills/ai-observability/references/1-begin.md b/context/skills/ai-observability/references/1-begin.md index 1ef2f318..4544cbd4 100644 --- a/context/skills/ai-observability/references/1-begin.md +++ b/context/skills/ai-observability/references/1-begin.md @@ -8,11 +8,15 @@ Pick the variant, then read the code. Do not edit anything in this step. ## Pick the variant -This skill ships 68 variants. Call `load_skill_menu` with `category: "ai-observability"`. That list is the source of truth. +This skill ships 69 variants. Call `load_skill_menu` with `category: "ai-observability"`. That list is the source of truth. Apply these rules in order. The first match wins. Frameworks wrap providers, and gateways look like OpenAI, so the order matters. -### 1. A framework wins over the provider under it +### 1. A Go project always takes OpenTelemetry + +A `go.mod` means Go. Go has no PostHog wrapper SDK, so every Go project takes `opentelemetry-go`, whatever SDK or framework makes the model calls. The bridge is `github.com/posthog/posthog-go/otel`; the install doc carries the code. The rules below apply to Python and Node projects only. + +### 2. A framework wins over the provider under it | Package in the manifest | Variant | |---|---| @@ -36,23 +40,23 @@ Apply these rules in order. The first match wins. Frameworks wrap providers, and Instrument the framework, not the provider below it. A provider instrumentor keeps the model calls and loses the agent, tool, and handoff structure. -### 2. An `openai` client with a base URL override means a gateway +### 3. An `openai` client with a base URL override means a gateway Most OpenAI-compatible providers ship no SDK. Apps call them with the `openai` package aimed at another host. Check the client constructor and `OPENAI_BASE_URL`. Common hosts are `api.groq.com`, `openrouter.ai`, `api.together.xyz`, and `localhost:11434`. Pick the variant that names the provider. The install shape matches plain OpenAI, but the provider name does not. Step 3 explains why that matters. -### 3. A plain provider SDK maps to that provider +### 4. A plain provider SDK maps to that provider `openai`, `anthropic`, `@anthropic-ai/sdk`, `google-genai`, `@google/genai`, `mistralai`, and `cohere` each have a variant. `boto3` with `bedrock-runtime` maps to `aws-bedrock`. -### 4. Anything else +### 5. Anything else - Several candidates and no framework: prefer the higher-level one. If it stays unclear, use `wizard_ask` with the candidates as options. - The app already emits its own OTel spans: `opentelemetry-{python,node}`. - No LLM SDK at all: `manual-capture`. -Language follows the manifest. A `package.json` means Node. A `pyproject.toml` or `requirements.txt` means Python. Framework variants have no language suffix. +Language follows the manifest. A `package.json` means Node. A `pyproject.toml` or `requirements.txt` means Python. A `go.mod` means Go, which rule 1 already routed to `opentelemetry-go`. Framework variants have no language suffix. Report the variant and the reason in a `[STATUS]` line, then call `install_skill` with the full id. diff --git a/context/skills/ai-observability/references/2-install.md b/context/skills/ai-observability/references/2-install.md index a97de2a9..5aaad832 100644 --- a/context/skills/ai-observability/references/2-install.md +++ b/context/skills/ai-observability/references/2-install.md @@ -33,6 +33,8 @@ Portkey also needs `portkey-ai`. AWS Bedrock has no wrapper client. It instruments the AWS SDK through OpenTelemetry, so its package list differs from every other provider. +`opentelemetry-go` declares `github.com/posthog/posthog-go/otel` and the OpenTelemetry SDK modules in `go.mod`. There are no Go instrumentation libraries for provider SDKs; step 3 hand-authors the `gen_ai.*` spans instead. + ## Do not - Do not run `npm install` or `pip install`. diff --git a/context/skills/ai-observability/references/3-instrument.md b/context/skills/ai-observability/references/3-instrument.md index 11a5ffbd..b265d489 100644 --- a/context/skills/ai-observability/references/3-instrument.md +++ b/context/skills/ai-observability/references/3-instrument.md @@ -20,6 +20,8 @@ Route the token and host through env vars with `set_env_values`. Reuse the names Agent frameworks use their own tracing hook in place of a wrapper. Take it from the install doc. Do not substitute an OTel instrumentor. +On the OpenTelemetry path (`opentelemetry-*` variants, which includes every Go project) there is no wrapper to swap. Register the PostHog span processor from the install doc, start one span per turn so the calls in it share a trace, and attach `$ai_session_id` and `posthog.distinct_id` the way the doc shows. The cardinality rules below apply unchanged. + ## Attach identity to every call Three per-call parameters carry the tree. Node uses camelCase. From 09eb2e69cb083c7f5222585ce93132959c31ae90 Mon Sep 17 00:00:00 2001 From: Carlos Marchal Date: Tue, 1 Sep 2026 18:35:16 +0200 Subject: [PATCH 2/2] fix(skills): keep the Go OTel path off wrapper-only instructions --- context/commandments.yaml | 1 + context/skills/ai-observability/description.md | 2 +- .../ai-observability/references/1-begin.md | 10 +++++++--- .../ai-observability/references/2-install.md | 4 +++- .../ai-observability/references/3-instrument.md | 16 ++++++++++++++-- .../ai-observability/references/4-verify.md | 1 + 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/context/commandments.yaml b/context/commandments.yaml index 167ba065..ff5ca2d0 100644 --- a/context/commandments.yaml +++ b/context/commandments.yaml @@ -334,4 +334,5 @@ commandments: - "AI Observability carve-out: this skill instruments LLM calls and is not product-analytics coverage. Do NOT add posthog.capture() events for user actions, captureException() error handlers, or a reverse proxy unless the user explicitly asks for them" - "AI Observability carve-out: only the wrapper-client and manual-capture install paths construct a PostHog client. The OTel and framework-hook paths have no client at all, so any rule in this file about the Posthog()/PostHog() constructor, exception autocapture, atexit/shutdown registration or flushing simply does not apply — never invent a client just to satisfy one" - "AI Observability carve-out: the $ai_* payload properties ($ai_input, $ai_output_choices, and the rest) intentionally carry user-generated prompt and completion text, so this file's PII rules do NOT apply to them. Those rules still govern every other property you set" + - "AI Observability carve-out: the Go OpenTelemetry path uses the nested github.com/posthog/posthog-go/otel module, not the core github.com/posthog/posthog-go SDK. This file's Go install and import line does not apply, and neither do its posthog.NewWithConfig client rules: the bridge registers a span processor and builds no PostHog client" - "AI Observability carve-out: read the PostHog key and host exactly as the variant's install doc reads them. A direct os.environ[\"POSTHOG_API_KEY\"] / process.env lookup already fails loudly and idiomatically when unset, which satisfies this file's missing-configuration rule — do NOT add a separate presence check, guard branch, or custom raise around a bootstrap that is only a few lines long" diff --git a/context/skills/ai-observability/description.md b/context/skills/ai-observability/description.md index 3902de55..d84c09a8 100644 --- a/context/skills/ai-observability/description.md +++ b/context/skills/ai-observability/description.md @@ -28,7 +28,7 @@ The linked install page carries the exact code blocks for this variant's languag ## Key principles - **Environment variables.** Read `` and `` from env, using the framework's env-var convention. Never hardcode either value. -- **The SDK wrapper is the default, not OpenTelemetry.** OTel makes the session tree awkward to build and maintain, so provider and gateway variants use PostHog's drop-in wrapper client. Reserve OTel for the `opentelemetry-*` variants and LlamaIndex, and never swap a framework's own tracing hook for an instrumentor. Go is the exception: it has no wrapper SDK, so every Go project uses the `opentelemetry-go` bridge. +- **The SDK wrapper is the default, not OpenTelemetry.** OTel makes the session tree awkward to build and maintain, so provider and gateway variants use PostHog's drop-in wrapper client. Reserve OTel for the `opentelemetry-*` variants and LlamaIndex, and never swap a framework's own tracing hook for an instrumentor. Go is the exception: it has no wrapper SDK, so model calls made from Go use the `opentelemetry-go` bridge. - **Minimal changes.** The wrapper swaps a client constructor and adds parameters to existing calls. Don't restructure the app, and don't wrap the setup in an init function or module globals. - **Match the docs.** Package names and wrapper imports change between AIO releases. The install page for this variant is the source of truth. - **Cardinality is what gets graded.** One `$ai_session_id` per conversation, one `posthog_trace_id` per turn, shared by every call in it. An id minted per call is worse than none — it looks instrumented and groups nothing. diff --git a/context/skills/ai-observability/references/1-begin.md b/context/skills/ai-observability/references/1-begin.md index 4544cbd4..e70a9b15 100644 --- a/context/skills/ai-observability/references/1-begin.md +++ b/context/skills/ai-observability/references/1-begin.md @@ -12,9 +12,13 @@ This skill ships 69 variants. Call `load_skill_menu` with `category: "ai-observa Apply these rules in order. The first match wins. Frameworks wrap providers, and gateways look like OpenAI, so the order matters. -### 1. A Go project always takes OpenTelemetry +### 1. Model calls made from Go take OpenTelemetry -A `go.mod` means Go. Go has no PostHog wrapper SDK, so every Go project takes `opentelemetry-go`, whatever SDK or framework makes the model calls. The bridge is `github.com/posthog/posthog-go/otel`; the install doc carries the code. The rules below apply to Python and Node projects only. +Go has no PostHog wrapper SDK, so a project whose model calls are in Go takes `opentelemetry-go`, whatever SDK or framework makes them. The bridge is `github.com/posthog/posthog-go/otel`, and the install doc carries the code. + +A `go.mod` on its own is not the signal. If the repo also has a `package.json`, `pyproject.toml`, or `requirements.txt`, find the call sites before you route. A Go service beside a Node or Python LLM app takes the rules below. If it stays unclear, use `wizard_ask`. + +The bridge needs Go 1.25 or newer. An older toolchain fails with `module requires go >= 1.25.0`. Say so in the report rather than raising the project's Go version yourself. ### 2. A framework wins over the provider under it @@ -56,7 +60,7 @@ Pick the variant that names the provider. The install shape matches plain OpenAI - The app already emits its own OTel spans: `opentelemetry-{python,node}`. - No LLM SDK at all: `manual-capture`. -Language follows the manifest. A `package.json` means Node. A `pyproject.toml` or `requirements.txt` means Python. A `go.mod` means Go, which rule 1 already routed to `opentelemetry-go`. Framework variants have no language suffix. +Language follows the manifest. A `package.json` means Node. A `pyproject.toml` or `requirements.txt` means Python. Go call sites went to `opentelemetry-go` under rule 1. Framework variants have no language suffix. Report the variant and the reason in a `[STATUS]` line, then call `install_skill` with the full id. diff --git a/context/skills/ai-observability/references/2-install.md b/context/skills/ai-observability/references/2-install.md index 5aaad832..49629f8b 100644 --- a/context/skills/ai-observability/references/2-install.md +++ b/context/skills/ai-observability/references/2-install.md @@ -35,9 +35,11 @@ AWS Bedrock has no wrapper client. It instruments the AWS SDK through OpenTeleme `opentelemetry-go` declares `github.com/posthog/posthog-go/otel` and the OpenTelemetry SDK modules in `go.mod`. There are no Go instrumentation libraries for provider SDKs; step 3 hand-authors the `gen_ai.*` spans instead. +Go is the exception to the no-package-manager and no-lockfile rules on this page. A `go.mod` require line with no matching `go.sum` entry fails the build with `missing go.sum entry for module`, so let the tool write both files: `go get github.com/posthog/posthog-go/otel go.opentelemetry.io/otel go.opentelemetry.io/otel/sdk`, then `go mod tidy`. Do not hand-edit `go.mod` or `go.sum`. + ## Do not - Do not run `npm install` or `pip install`. -- Do not edit the lockfile. +- Do not edit the lockfile. On Go, `go get` and `go mod tidy` write `go.mod` and `go.sum` for you. - Do not upgrade the vendor SDK. - Do not add OpenTelemetry to a wrapper variant. diff --git a/context/skills/ai-observability/references/3-instrument.md b/context/skills/ai-observability/references/3-instrument.md index b265d489..ac3530ad 100644 --- a/context/skills/ai-observability/references/3-instrument.md +++ b/context/skills/ai-observability/references/3-instrument.md @@ -20,11 +20,20 @@ Route the token and host through env vars with `set_env_values`. Reuse the names Agent frameworks use their own tracing hook in place of a wrapper. Take it from the install doc. Do not substitute an OTel instrumentor. -On the OpenTelemetry path (`opentelemetry-*` variants, which includes every Go project) there is no wrapper to swap. Register the PostHog span processor from the install doc, start one span per turn so the calls in it share a trace, and attach `$ai_session_id` and `posthog.distinct_id` the way the doc shows. The cardinality rules below apply unchanged. +### The OpenTelemetry path + +On the `opentelemetry-*` variants, which is where Go call sites land, there is no wrapper to swap and no PostHog client to build. Register the PostHog span processor from the install doc on the tracer provider the app already owns. Flush before exit with `ForceFlush` or `Shutdown`, or the buffered spans are lost. + +The processor forwards a span only when its **name** or one of its **span attribute keys** starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`. It drops every other span silently, with no error. Two things follow: + +- Start one span per turn so the calls in it share a trace, and make that turn span pass the filter. Name it in the `gen_ai.` or `ai.` namespace, or give it a `gen_ai.*` attribute. A turn span named `handle turn` never reaches PostHog, and the generations under it arrive with no root. +- `$ai_session_id` begins with `$`, so it does not satisfy the filter on its own. The filter reads span attributes only, never resource attributes. Set `$ai_session_id` on every span in the conversation, and carry `posthog.distinct_id` the way the install doc shows. + +Cardinality is the same as the wrapper path: one `$ai_session_id` per conversation, one trace per turn. The OTel trace carries the grouping, so the per-call parameters in the next section do not exist here. ## Attach identity to every call -Three per-call parameters carry the tree. Node uses camelCase. +On the wrapper path, three per-call parameters carry the tree. Node uses camelCase. The OpenTelemetry path has none of them; the section above covers it. | Parameter | Holds | Cardinality | |---|---|---| @@ -56,6 +65,8 @@ The wrapper records the model call. It never sees the tool dispatch loop, so not If the app registers tools, capture each run as an `$ai_span` event with `posthog.capture()`. Give it the turn's `$ai_trace_id` so the span joins the trace. The install doc lists the span properties. +On the OpenTelemetry path there is no PostHog client to call. Record the tool run as a child span of the turn span instead, with `gen_ai.*` attributes so it passes the AI span filter. A plain span named after the tool is dropped. + Put the capture next to the existing dispatch. Do not restructure the tool loop. Agent frameworks and the Vercel AI SDK emit tool spans on their own. Add nothing on those variants. @@ -66,6 +77,7 @@ An app that registers no tools has no spans. That is a complete result, not a ga - Do not restructure the app. This step swaps a constructor and adds arguments to calls. - Do not omit `posthog_trace_id` and expect the calls to group. +- Do not leave a turn span that fails the AI span filter on the OpenTelemetry path. - Do not mint a session id per call or per turn. - Do not leave a gateway reporting `$ai_provider` as `openai`. - Do not add spans when the app registers no tools. diff --git a/context/skills/ai-observability/references/4-verify.md b/context/skills/ai-observability/references/4-verify.md index 1c07b39d..e5d4abc6 100644 --- a/context/skills/ai-observability/references/4-verify.md +++ b/context/skills/ai-observability/references/4-verify.md @@ -42,6 +42,7 @@ Before you hand over, run the import the code depends on, such as `python3 -c "f | `$ai_provider` says `openai` on a gateway | the per-call override is missing | | Anonymous person | `posthog_distinct_id` is missing | | No tool spans | the `$ai_span` captures are missing or carry another trace id | +| A trace with no root, on the OTel path | the turn span fails the bridge's AI filter, so the processor drops it | | Nothing arrives | the code still calls the vendor client, the key or host is unset, or a short script exits before the flush | ## Do not