Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Maple Agent Tracing Genkit

ASecurity

Trace Genkit (TypeScript/Node.js) agents with Maple: export Genkit's OpenTelemetry spans to Maple, map its genkit:* attributes to the GenAI conventions with a span processor, and stamp a conversation id so each chat is one Agent Session with transcript, tool calls and tokens. Covers flows, ai.generate, generateStream and beta defineAgent chats. Triggers on 'trace my genkit agent', 'add Maple to genkit', 'agent sessions for genkit', 'OpenTelemetry for genkit', 'firebase genkit tracing'.

1,799 stars
0 votes
0 copies
0 views
Added 10/1/2026
developmentjavascripttypescriptpythonrustgojavabashnextjsnodeexpress

Works with

cliapimcp

Security Analysis

A96/100
mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned 10/1/2026

$npx -y skills add MapleTechLabs/maple --skill maple-agent-tracing-genkit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Maple Agent Tracing Genkit?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Maple Agent Tracing Genkit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mapletechlabs-maple-agent-tracing-genkit/badge)](https://www.skillsdirectory.com/skills/mapletechlabs-maple-agent-tracing-genkit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: maple-agent-tracing-genkit
description: "Trace Genkit (TypeScript/Node.js) agents with Maple: export Genkit's OpenTelemetry spans to Maple, map its genkit:* attributes to the GenAI conventions with a span processor, and stamp a conversation id so each chat is one Agent Session with transcript, tool calls and tokens. Covers flows, ai.generate, generateStream and beta defineAgent chats. Triggers on 'trace my genkit agent', 'add Maple to genkit', 'agent sessions for genkit', 'OpenTelemetry for genkit', 'firebase genkit tracing'."
---

# Maple agent tracing: Genkit

## Goal

One conversation = one Maple Agent Session, one turn per flow run, with the transcript, every model call (model, tokens) and every tool call (name, args, result, failures).

Span tree per flow run:

```text
supportChat            genkit:metadata:subtype=flow, genkit:isRoot=true  -> invoke_agent
└── generate           genkit:type=util (one nested generate per tool-loop step, left unmapped)
    ├── googleai/gemini-2.5-flash   subtype=model  -> chat
    ├── getWeather                  subtype=tool   -> execute_tool
    └── generate
        └── googleai/gemini-2.5-flash   subtype=model  -> chat
```

Beta agents (`ai.defineAgent()` / `defineCustomAgent` / `definePromptAgent` from `genkit/beta`): root span has subtype `agent` and `genkit:metadata:agent:sessionId` (Genkit's session id), which the processor uses as the conversation id. Under it: `runTurn-<n>` (flowStep), `render` (promptTemplate), `generate`, model, tool spans. One trace per `chat.send()`.

Known gaps (tell the user, don't try to fix): cost shows as unpriced; `gen_ai.provider.name` is the Genkit plugin prefix (`googleai`, `vertexai`, `openai`, `anthropic`...) rather than the semconv value (`gcp.gemini`...), which is only a label in Maple; media parts are left out of transcripts; `execute_tool` spans have no `gen_ai.tool.call.id` (Genkit doesn't put the call ref on the tool span; the transcript still pairs calls and results through the ids in the model messages when the model plugin sets `ref`).

## Step 0: Detect

- `genkit` version in `package.json` / lockfile: need `>= 1.22` (`disableGenkitOTelInitialization` was added in 1.22). Older: upgrade Genkit first. Node.js >= 20.
- Go or Python Genkit: stop; this skill covers TypeScript/JavaScript only. Tell the user.
- Existing OpenTelemetry: search for `NodeSDK`, `NodeTracerProvider`, `registerOTel`, `@vercel/otel`, `Sentry.init`, `enableTelemetry(`, `enableFirebaseTelemetry(`, `enableGoogleCloudTelemetry(`, `ENABLE_FIREBASE_MONITORING`.
  - An SDK/provider already exists: reuse it. Add `GenkitForMaple` and one Maple exporting processor to it. Never start a second SDK.
  - `enableTelemetry({...})` from `genkit/tracing` with custom processors: remove it. Genkit's `enableTelemetry` builds its own bundled `@opentelemetry/sdk-node` 0.52 / `sdk-trace-base` 1.25, and current (2.x) exporters crash inside it (`Cannot read properties of undefined (reading 'name')` on `instrumentationScope`). Don't pass Maple's exporter there.
  - `enableFirebaseTelemetry()` / `enableGoogleCloudTelemetry()` / `ENABLE_FIREBASE_MONITORING=true`: `disableGenkitOTelInitialization()` makes these no-ops (they go through `enableTelemetry`). Ask the user whether Google Cloud trace export may stop. If they need both, stop and tell them; don't wire two SDKs.
- Find every flow (`ai.defineFlow(`), direct `ai.generate(` / `ai.generateStream(` / `prompt(` call site, beta agents (`defineAgent(`, `.chat(`), and how the app deploys (plain Node server, Express `startFlowServer`/`expressHandler`, Next.js `@genkit-ai/next`, Cloud Functions for Firebase `onCallGenkit`, Cloud Run). Find each conversation's id (chat id, thread id, session row).

## Step 1: Key and region

- US endpoint `https://ingest.maple.dev`, EU endpoint `https://ingest.eu.maple.dev`. Header `Authorization=Bearer <key>`.
- Key in the user's prompt: use it. No key: use the literal `MAPLE_TEST` (ingest accepts and discards it) and tell the user to replace it with their key from Settings → Ingestion.
- Private `maple_sk_` keys never go in browser code. Ingest keys are write-only.
- Follow the repo's existing secret/env convention (`.env`, Firebase `defineSecret`, Secret Manager). If there is none, inlining the ingest key is acceptable.
- A 401 `ingest_unauthorized` / "Invalid ingest key" with a key you trust usually means the key belongs to the other region (keys are region-bound): try the other endpoint.

## Step 2: Install

```bash
npm install genkit @opentelemetry/sdk-node @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto
```

Use the repo's package manager. `@opentelemetry/api` arrives as a peer; add it explicitly only if the package manager doesn't install peers. Env (or the repo's equivalent):

```bash
OTEL_SERVICE_NAME=support-agent
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=production
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.maple.dev
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <key>"
```

Inlining instead of env: `new OTLPTraceExporter({ url: "https://ingest.maple.dev/v1/traces", headers: { authorization: "Bearer <key>" } })` (the full `/v1/traces` path is needed when passing `url`).

- The app loads `.env` (`dotenv`, `--env-file`): load it at the top of `instrumentation.ts` (`import "dotenv/config"` as its first line) or run with `--env-file`. The exporter reads the `OTEL_*` vars when it is constructed; otherwise it silently targets `localhost:4318` with no key.
- Building the header from a variable (`Bearer ${process.env.MAPLE_INGEST_KEY}`): when it is unset, log one warning (`MAPLE_INGEST_KEY is not set; Maple telemetry export is disabled`) and skip the Maple exporter so the app runs normally. Never throw over the key, and never let it become `Bearer undefined` (opaque 401).

## Step 3: The span processor

Create `genkit-for-maple.ts` next to the entry point, verbatim:

```ts
// genkit-for-maple.ts
import type { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base"

type Part = {
	text?: string
	reasoning?: string
	toolRequest?: { name: string; ref?: string; input?: unknown }
	toolResponse?: { name: string; ref?: string; output?: unknown }
}
type Message = { role: string; content: Part[] }

// Genkit message parts to OpenTelemetry GenAI parts. Media parts are left out.
function toPart({ text, reasoning, toolRequest, toolResponse }: Part) {
	if (text !== undefined) return { type: "text", content: text }
	if (reasoning !== undefined) return { type: "reasoning", content: reasoning }
	if (toolRequest) {
		return { type: "tool_call", id: toolRequest.ref, name: toolRequest.name, arguments: toolRequest.input }
	}
	if (toolResponse) return { type: "tool_call_response", id: toolResponse.ref, response: toolResponse.output }
	return undefined
}

const toParts = (content: Part[]) => content.map(toPart).filter((part) => part !== undefined)

function toMessages(messages: Message[]) {
	return messages.map((m) => ({ role: m.role === "model" ? "assistant" : m.role, parts: toParts(m.content) }))
}

/** Adds the gen_ai.* attributes Maple reads to Genkit's flow, model and tool spans. */
export class GenkitForMaple implements SpanProcessor {
	onStart() {}

	onEnd(span: ReadableSpan) {
		const attrs = span.attributes
		const json = (key: string) => {
			const value = attrs[key]
			return typeof value === "string" ? JSON.parse(value) : undefined
		}
		const name = String(attrs["genkit:name"])

		switch (attrs["genkit:metadata:subtype"]) {
			case "flow":
			case "agent": {
				Object.assign(attrs, {
					"gen_ai.operation.name": "invoke_agent",
					"gen_ai.agent.name": name,
				})
				// Set in your flow, or by Genkit for defineAgent() chats
				const conversationId = attrs["genkit:metadata:conversationId"] ?? attrs["genkit:metadata:agent:sessionId"]
				if (conversationId !== undefined) attrs["gen_ai.conversation.id"] = conversationId
				break
			}
			case "model": {
				const input = json("genkit:input")
				const output = json("genkit:output")
				const [provider, ...model] = name.split("/")
				const messages: Message[] = input?.messages ?? []
				const system = messages.filter((m) => m.role === "system").flatMap((m) => toParts(m.content))
				Object.assign(attrs, {
					"gen_ai.operation.name": "chat",
					"gen_ai.provider.name": provider,
					"gen_ai.request.model": model.join("/") || name,
					"gen_ai.input.messages": JSON.stringify(toMessages(messages.filter((m) => m.role !== "system"))),
				})
				if (system.length > 0) attrs["gen_ai.system_instructions"] = JSON.stringify(system)
				if (output?.message) {
					attrs["gen_ai.output.messages"] = JSON.stringify(
						toMessages([output.message]).map((m) => ({ ...m, finish_reason: output.finishReason })),
					)
				}
				if (output?.finishReason) attrs["gen_ai.response.finish_reasons"] = [output.finishReason]
				if (output?.usage?.inputTokens !== undefined) attrs["gen_ai.usage.input_tokens"] = output.usage.inputTokens
				if (output?.usage?.outputTokens !== undefined) attrs["gen_ai.usage.output_tokens"] = output.usage.outputTokens
				break
			}
			case "tool": {
				Object.assign(attrs, {
					"gen_ai.operation.name": "execute_tool",
					"gen_ai.tool.name": name,
					"gen_ai.tool.call.arguments": attrs["genkit:input"] ?? "{}",
				})
				const result = json("genkit:output")
				if (result !== undefined) {
					attrs["gen_ai.tool.call.result"] = typeof result === "string" ? result : JSON.stringify(result)
				}
				break
			}
		}
	}

	forceFlush() {
		return Promise.resolve()
	}

	shutdown() {
		return Promise.resolve()
	}
}
```

It maps:

| Genkit span (`genkit:metadata:subtype`) | Adds |
| --- | --- |
| `flow`, `agent` | `gen_ai.operation.name=invoke_agent`, `gen_ai.agent.name=<genkit:name>`, `gen_ai.conversation.id` from `genkit:metadata:conversationId` or `genkit:metadata:agent:sessionId` |
| `model` | `chat`, `gen_ai.provider.name` (prefix before `/`), `gen_ai.request.model` (rest), `gen_ai.input.messages` / `gen_ai.system_instructions` from `genkit:input.messages`, `gen_ai.output.messages` + `gen_ai.response.finish_reasons` from `genkit:output`, `gen_ai.usage.input_tokens` / `output_tokens` from `genkit:output.usage` |
| `tool` | `execute_tool`, `gen_ai.tool.name`, `gen_ai.tool.call.arguments` (= `genkit:input`), `gen_ai.tool.call.result` (= `genkit:output`, string results as plain text) |

Message conversion: role `model` → `assistant`; parts `{text}` → `text`, `{reasoning}` → `reasoning`, `{toolRequest:{name,ref,input}}` → `tool_call`, `{toolResponse:{name,ref,output}}` → `tool_call_response`; `media`, `data`, `custom` parts are dropped.

Notes:
- It mutates `span.attributes` in `onEnd`. That works because the exporting processor reads the same object; list `GenkitForMaple` BEFORE the exporting processor (required with `SimpleSpanProcessor`, which exports inside `onEnd`).
- Optional, only if the user asks for cache/reasoning token detail: `output.usage.cachedContentTokens` → `gen_ai.usage.cache_read.input_tokens`, `output.usage.thoughtsTokens` → `gen_ai.usage.reasoning.output_tokens`. Gemini models (`googleai/`, `vertexai/`) report `outputTokens` without thoughts: for them set `output_tokens` to `outputTokens + thoughtsTokens`. Keep `input_tokens` as Genkit reports it.
- Optional provider label mapping (`googleai` → `gcp.gemini`, `vertexai` → `gcp.vertex_ai`): cosmetic, skip unless asked.
- Leave `genkit:*` attributes in place; they are what the Genkit Developer UI and Google Cloud views read.
- Typecheck the file with the repo's `tsc` (it passes `strict`).

## Step 4: Start OpenTelemetry

Create `instrumentation.ts`:

```ts
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
import { NodeSDK } from "@opentelemetry/sdk-node"
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"
import { disableGenkitOTelInitialization } from "genkit/tracing"
import { GenkitForMaple } from "./genkit-for-maple"

export const spanProcessor = new BatchSpanProcessor(new OTLPTraceExporter())
export const sdk = new NodeSDK({ spanProcessors: [new GenkitForMaple(), spanProcessor] })

if (process.env.GENKIT_ENV !== "dev") {
	disableGenkitOTelInitialization()
	sdk.start()
}
```

- `import "./instrumentation"` as the FIRST line of every entry point (server, worker, CLI, Cloud Function index). It must run before Genkit's first span: otherwise Genkit's own SDK registers the global tracer provider first and Maple's never receives spans. Adjust the import extension/style to the repo (ESM `.js` suffix, `tsx`, bundler).
- Why `disableGenkitOTelInitialization()`: without it Genkit lazily starts a second, bundled NodeSDK on its first span. With Maple's SDK started first that second one mostly loses the global registration race, but it's two SDKs; disable it.
- `GENKIT_ENV=dev` is set by `genkit start` (Developer UI). Genkit's own SDK then ships traces to the Developer UI's telemetry server; Maple's SDK can't feed it (Genkit's `TraceServerExporter` reads 1.x span fields). So in dev: Developer UI traces, nothing to Maple. If the user wants Maple in dev too, drop the `if` and tell them the Developer UI trace view goes empty.
- Existing provider/SDK: add `new GenkitForMaple()` and the Maple `BatchSpanProcessor` to its span processors (before any exporting processor for GenkitForMaple), keep `disableGenkitOTelInitialization()`, don't create `NodeSDK`.
- Next.js with `@vercel/otel`: pass `spanProcessors: [new GenkitForMaple(), new BatchSpanProcessor(new OTLPTraceExporter({ url, headers }))]` to `registerOTel` and call `disableGenkitOTelInitialization()` in `register()`. Untested; verify spans arrive.
- `NodeSDK()` with `spanProcessors` doesn't build exporters from `OTEL_TRACES_EXPORTER`; the explicit `OTLPTraceExporter` still reads `OTEL_EXPORTER_OTLP_ENDPOINT` / `HEADERS`. It also starts OTLP metric and log exporters from env (default `otlp`); set `OTEL_METRICS_EXPORTER=none` / `OTEL_LOGS_EXPORTER=none` if the repo doesn't want them.

Genkit starts a NEW TRACE for every root Genkit action (it passes `root: true`), even inside an HTTP server span. That's fine for Maple (one trace per turn). If the user wants flows nested under their HTTP spans, `disableOTelRootSpanDetection()` from `genkit/tracing` changes it; not needed for Agent Sessions.

## Step 5: Conversation id (session)

Every turn must run inside a flow (or a beta agent). In each flow that handles a conversation, call at the start:

```ts
import { setCustomMetadataAttribute } from "genkit/tracing"

setCustomMetadataAttribute("conversationId", chatId)
```

- It writes to the CURRENT Genkit action, so call it directly in the flow body, not inside a tool or a nested `ai.run()` step. It throws outside any Genkit action.
- `ai.generate()` called outside a flow (e.g. straight from an Express route): wrap the call in a flow (`ai.defineFlow`) and call the flow from the route. Without a flow there is no `invoke_agent` span and no session id; each call becomes its own `trace:<id>` session.
- Id: stable per conversation, unique across conversations. Never a module constant, `Date.now()` per call or a per-process id. If the app has no id, create one when the conversation is created and persist it with it.
- Flows exposed via `startFlowServer` / `expressHandler` / `onCallGenkit` / `appRoute`: the client must send the chat id in the flow input; add it to `inputSchema` if missing.
- Beta agents (`defineAgent` etc.): nothing to add; `genkit:metadata:agent:sessionId` is used.
- The flow name is the agent name in Maple (`gen_ai.agent.name`). Give each distinct agent its own flow name.
- Sub-agents: a flow called from inside another flow or a tool also becomes an `invoke_agent` span named after that flow (expected to show as a sub-agent lane; not verified in Maple). Don't set the conversation id on inner flows.
- Don't set `gen_ai.conversation.id` or `maple_ai.session.id` on spans yourself; the processor does it.

## Step 6: Content, errors, flush

- Content: Genkit always records `genkit:input` / `genkit:output` (full prompts, replies, tool args/results). There is no Genkit switch. If the user needs content kept out of Maple, delete `genkit:input`/`genkit:output` in the processor after mapping and skip the message/argument/result attributes; tell them transcripts will be empty.
- Never set `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` / `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT`: a truncated `genkit:input` makes `JSON.parse` in the processor throw inside `span.end()`.
- Tool failures: a tool that throws ends with status `ERROR` (Maple counts it failed) and aborts the whole `generate` / flow (Genkit doesn't feed the error back to the model). Don't change tool error behavior for tracing; tools that return `{ error }` payloads show as successful, which is accurate to what the model saw.
- Streaming (`ai.generateStream`, `flow.stream()`): model spans end with the full output and usage; read the stream to the end before flushing.
- Flush:
  - Scripts/CLIs: `await sdk.shutdown().catch((err) => console.error("telemetry flush failed", err))` in a `finally`. `shutdown()` rejects when an export failed; the `.catch` keeps a Maple outage from crashing the app.
  - Long-running servers (Cloud Run, plain Node): `process.on("SIGTERM", () => sdk.shutdown().catch((err) => console.error("telemetry flush failed", err)))`; nothing per request.
  - Serverless handlers you control: `await spanProcessor.forceFlush()` after the flow returns (the flow span ends when the flow returns, so flushing inside the flow misses it).
  - Cloud Functions for Firebase `onCallGenkit(flow)`: you can't hook after the flow; the instance may be throttled after the response, so the last batch can be delayed or lost. Tell the user; if it matters, wrap with `onCall` and call the flow then `forceFlush()` before returning. Untested.

## Step 7: Verify

Run one conversation with 2+ turns under the same id, one tool call, plus a second conversation. If the app has no scriptable entry point (server, Developer UI only), write a small driver for this that calls the flow directly: one conversation id, 2+ turns, at least one tool call, flush before exit. Then check (Maple MCP `list_agent_sessions` / `get_agent_session`, or the Agent Sessions page, ~30 s after the run):

- One session per conversation (id = your conversation id), not `trace:<id>` sessions; the second conversation is separate.
- Framework shows **Genkit**.
- Turns = number of flow runs; each trace has `invoke_agent` (flow name) → `generate` → model (`chat`) and tool (`execute_tool`) spans.
- Transcript shows user messages, assistant replies, tool calls with arguments and results; turn labels are the user's messages.
- Every model span has input and output tokens (if the model plugin reports usage).
- A throwing tool is failed; successful tools are not.
- No attribute contains the provider API key or `Bearer `.
- Under `genkit start`, the Developer UI still shows traces (and nothing reaches Maple).
- The process exited cleanly and no turn is missing (flush ran).

Without Maple access: the run exits with no export errors on stderr (`OTLPExporterError`, `Failed to export`, 401 lines) AND a local check shows the spans. Silence alone proves nothing (no spans also looks silent). Local check: point `OTEL_EXPORTER_OTLP_ENDPOINT` at a small HTTP server and swap in `@opentelemetry/exporter-trace-otlp-http` with `OTEL_EXPORTER_OTLP_PROTOCOL=http/json` to read the spans as JSON; confirm `gen_ai.operation.name` on flow/model/tool spans and `gen_ai.conversation.id` equal across turns. With the Maple MCP: `list_agent_sessions` with `search=<conversation id>` returns one row.

## Do not

- Do not add a second GenAI tracer that also exports to Maple (OpenLLMetry/OpenInference Google GenAI or OpenAI instrumentations for the same calls): model calls get recorded twice.

Attribution

MapleTechLabsMapleTechLabs
View sourceSee grades on GitHubMore from MapleTechLabs →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Clean Code

Pragmatic coding standards - concise, direct, no over-engineering, no unnecessary comments

304955 votes

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

286712 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2222 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Writing Plans

Use when you have a spec or requirements for a multi-step task, before touching code

2927051 votes
View all in development →