@heystack/otel 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -20
- package/dist/llm-enrich.d.ts +63 -1
- package/dist/llm-enrich.js +292 -19
- package/dist/node.js +2 -2
- package/dist/workers-agents.d.ts +49 -0
- package/dist/workers-agents.js +110 -0
- package/dist/workers.d.ts +61 -3
- package/dist/workers.js +442 -63
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -13,14 +13,15 @@ Always read your key from the environment — never paste it into source:
|
|
|
13
13
|
HEYSTACK_API_KEY=sk_live_…
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
> **
|
|
16
|
+
> **Use `@heystack/otel` `>=0.14.0`.** 0.14 makes LLM `gen_ai.*` telemetry work under Cloudflare native tracing (it was silently absent before), adds Mistral + AI-gateway (`ai.gateways`) detection and streaming token usage, and moves to the OpenTelemetry 2.x SDK (clears downstream Dependabot alerts). 0.13 made Cloudflare's native trace tree authoritative. See [Migration](#migration--versioning) below.
|
|
17
17
|
|
|
18
18
|
## Runtime matrix
|
|
19
19
|
|
|
20
20
|
| Runtime | Import | Notes |
|
|
21
21
|
| --- | --- | --- |
|
|
22
22
|
| Next.js — **any** deploy target (Vercel/Node **and** Cloudflare/OpenNext) | `@heystack/otel/next` | `registerHeystack` in `instrumentation.ts`. Auto-detects Node vs Cloudflare workerd and picks the right exporter. No-op on Edge. |
|
|
23
|
-
| Standalone Cloudflare Workers (hand-written `export default { fetch }`) | `@heystack/otel/workers` | `instrument()`
|
|
23
|
+
| Standalone Cloudflare Workers (hand-written `export default { fetch }`) | `@heystack/otel/workers` | Native-first `instrument()` wrapper; Cloudflare owns OTLP export and automatic platform spans. |
|
|
24
|
+
| Cloudflare Agents SDK / Agents Tail Worker | `@heystack/otel/workers/agents` | Structured Agents lifecycle events, trace correlation, redaction, and Tail Worker adapter. |
|
|
24
25
|
| Node / Express / Fastify / NestJS (long-running server) | `@heystack/otel/node` | `initHeystack`: auto-instrumentations + graceful shutdown. |
|
|
25
26
|
| Browser (SPA / any web frontend) | `@heystack/otel/web` | `instrumentWeb`: session replay + opt-in browser distributed tracing (`tracing: true`) that emits CLIENT spans + propagates W3C `traceparent` (browser→API shows as one trace). No-op on the server (SSR-safe). |
|
|
26
27
|
| Anywhere (pure helpers) | `@heystack/otel` | `buildExporterConfig`, types. No Node SDK loaded. |
|
|
@@ -108,6 +109,64 @@ export async function register() {
|
|
|
108
109
|
|
|
109
110
|
## Standalone Cloudflare Workers
|
|
110
111
|
|
|
112
|
+
### Preferred: Cloudflare-native OTel export
|
|
113
|
+
|
|
114
|
+
Create separate `heystack-traces` and `heystack-logs` OTLP destinations in Cloudflare's Workers Observability dashboard. Use `https://ingest.heystack.dev/v1/traces` and `/v1/logs`, each with `Authorization: Bearer <ingest-key>`, then configure:
|
|
115
|
+
|
|
116
|
+
```toml
|
|
117
|
+
[observability.traces]
|
|
118
|
+
enabled = true
|
|
119
|
+
destinations = ["heystack-traces"]
|
|
120
|
+
head_sampling_rate = 1
|
|
121
|
+
persist = false
|
|
122
|
+
|
|
123
|
+
[observability.logs]
|
|
124
|
+
enabled = true
|
|
125
|
+
destinations = ["heystack-logs"]
|
|
126
|
+
head_sampling_rate = 1
|
|
127
|
+
persist = false
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Cloudflare creates the invocation and supported platform/binding spans and exports trace-correlated logs. `instrument()` detects `ctx.tracing` and adds Heystack business/identity attributes to that same tree. In this mode it creates no OTel provider, makes no Heystack export request, does not proxy bindings, and does not require `HEYSTACK_API_KEY` in the Worker. The only `fetch` patch it installs is a **lean, LLM-only** one (0.14+): outbound calls to a detected LLM provider or a configured AI gateway get a native `<operation> <provider>` span (e.g. `chat mistral`) with `gen_ai.*` attributes — every other subrequest passes through untouched, since Cloudflare already records it. See [AI / LLM observability](#ai--llm-observability).
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { instrument, withCloudflareSpan } from "@heystack/otel/workers";
|
|
134
|
+
|
|
135
|
+
const worker = {
|
|
136
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
137
|
+
return withCloudflareSpan(ctx, "order.load", { "order.id": "o1" }, async () => {
|
|
138
|
+
return Response.json(await env.DB.prepare("SELECT 1").first());
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export default instrument(worker, {
|
|
144
|
+
service: "my-worker",
|
|
145
|
+
nativeTracing: "auto", // default
|
|
146
|
+
getUser: (request) => ({ id: request.headers.get("x-user-id") ?? undefined }),
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The wrapper also enters native spans for default-export queue and scheduled handlers. `withCloudflareSpan(ctx, ...)` remains native across arbitrary `await` boundaries and does not require `nodejs_compat`. `withSpan(...)` routes to the native tracer whenever its active context is available.
|
|
151
|
+
|
|
152
|
+
### Cloudflare Agents SDK
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { Agent } from "agents";
|
|
156
|
+
import { createCloudflareAgentsObservability } from "@heystack/otel/workers/agents";
|
|
157
|
+
|
|
158
|
+
const observability = createCloudflareAgentsObservability();
|
|
159
|
+
export class SupportAgent extends Agent<Env> {
|
|
160
|
+
override observability = observability;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every Agents SDK RPC, state, message/tool, chat/recovery, fiber, scheduling, lifecycle, workflow, MCP, and email event becomes a structured, trace-correlated native log. Common secret and content fields are redacted. For production diagnostics channels, attach a Tail Worker using `createCloudflareAgentsTailHandler()` and enable the same native log destination on it.
|
|
165
|
+
|
|
166
|
+
### Direct OTLP fallback (non-native runtimes only)
|
|
167
|
+
|
|
168
|
+
Set `nativeTracing: "direct"` only when `ctx.tracing` and Cloudflare native export are unavailable. The remainder of this section describes that compatibility path. It requires `wrangler secret put HEYSTACK_API_KEY`; do not run it alongside a native trace destination because that creates duplicate trace trees.
|
|
169
|
+
|
|
111
170
|
For a hand-written Worker (`export default { fetch }`), wrap the handler with `instrument()`:
|
|
112
171
|
|
|
113
172
|
```ts
|
|
@@ -121,6 +180,7 @@ export default instrument(
|
|
|
121
180
|
},
|
|
122
181
|
{
|
|
123
182
|
service: "my-worker", // apiKey defaults to env.HEYSTACK_API_KEY
|
|
183
|
+
nativeTracing: "direct", // compatibility fallback only
|
|
124
184
|
getUser: (req) => ({
|
|
125
185
|
id: req.headers.get("x-user-id") ?? undefined,
|
|
126
186
|
}),
|
|
@@ -135,9 +195,9 @@ export default instrument(
|
|
|
135
195
|
export default instrument(withOtherMiddleware(worker), { service: "my-worker" });
|
|
136
196
|
```
|
|
137
197
|
|
|
138
|
-
Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY
|
|
198
|
+
Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY`, and set `nativeTracing: "direct"` in the config.
|
|
139
199
|
|
|
140
|
-
> **
|
|
200
|
+
> **Direct mode requires `nodejs_compat` on workerd.** The direct SDK uses `AsyncLocalStorage` for per-request context isolation. Native mode does not require it. Add to `wrangler.toml`:
|
|
141
201
|
> ```toml
|
|
142
202
|
> compatibility_flags = ["nodejs_compat"]
|
|
143
203
|
> ```
|
|
@@ -148,11 +208,12 @@ Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY`.
|
|
|
148
208
|
| Option | Type | Notes |
|
|
149
209
|
| --- | --- | --- |
|
|
150
210
|
| `service` | `string` | **Required.** Service name that appears in the Heystack console. |
|
|
211
|
+
| `nativeTracing` | `"auto" \| "native" \| "direct"` | `auto` (default) uses Cloudflare's native tree when `ctx.tracing` exists. `direct` forces the compatibility exporter. |
|
|
151
212
|
| `apiKey` | `string?` | Defaults to `env.HEYSTACK_API_KEY`. |
|
|
152
213
|
| `getUser` | `(req: Request) => { id?, session?, requestId? } \| undefined` | Called per request. `id` → `enduser.id`, `session` → `session.id`, `requestId` → `http.request.id` (falls back to the `cf-ray` header). |
|
|
153
214
|
| `instrumentBindings` | `boolean \| string[]` | `true` = auto child spans for all detected D1 / KV / R2 / Vectorize / Workers AI / Queue producer / Service binding bindings; `string[]` = only the named bindings. Default `false`. |
|
|
154
215
|
| `sampling` | `{ rate?: number } \| { remote: true }` | Head-sampling configuration. `{ rate }`: keep a deterministic fraction of fresh root traces (0–1; default `1` = keep all). `{ remote: true }`: fetch the rate from the Heystack config endpoint instead — lets you change it centrally without redeploying. Cold isolates keep all traffic until the first config fetch resolves; fails open if the config can't be reached. Parent-respecting in both modes: a request arriving with a sampled `traceparent` is always recorded. See [Head sampling](#head-sampling) below. |
|
|
155
|
-
| `ai` | `{ captureContent?: boolean; redact?: (text: string) => string; maxContentChars?: number }` | LLM/gen_ai capture for outbound calls to known providers. See [AI / LLM observability](#ai--llm-observability) below. |
|
|
216
|
+
| `ai` | `{ captureContent?: boolean; redact?: (text: string) => string; maxContentChars?: number; gateways?: Array<{ hostSuffix: string; pathPattern: string }> }` | LLM/gen_ai capture for outbound calls to known providers (works under native tracing and the direct exporter). `gateways` declares AI gateways/proxies you call instead of the provider host (e.g. Wafer). See [AI / LLM observability](#ai--llm-observability) below. |
|
|
156
217
|
| `waitUntil` | `(p: Promise<unknown>) => void` | Override the isolate keep-alive hook; defaults to the auto-detected `ctx.waitUntil`. |
|
|
157
218
|
| `version` | `string?` | Release identifier → `service.version` resource attribute. Powers release health + suspect release. See [Release & commit attribution](#release--commit-attribution-version--build). |
|
|
158
219
|
| `build` | `string?` | Commit SHA → `service.build` resource attribute. Powers suspect commit (deep-links to the commit when a repo URL is set in the console). |
|
|
@@ -212,29 +273,50 @@ These attributes are set automatically on every SERVER span from request metadat
|
|
|
212
273
|
|
|
213
274
|
### AI / LLM observability
|
|
214
275
|
|
|
215
|
-
When your Worker calls an LLM API, `instrument()` automatically attaches [OpenTelemetry gen_ai semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) to the outbound
|
|
276
|
+
When your Worker calls an LLM API, `instrument()` automatically attaches [OpenTelemetry gen_ai semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) to the outbound call. No extra code is needed — detection is based on the target URL. It works on **both tracing planes**:
|
|
216
277
|
|
|
217
|
-
**
|
|
278
|
+
- **Cloudflare native tracing** (`nativeTracing: "auto" | "native"`, the default) — the call becomes a native span named `<operation> <provider>` (e.g. `chat mistral`, `chat google`) in Cloudflare's trace tree, carrying the `gen_ai.*` attributes below plus `server.address` and `http.response.status_code`. Non-LLM subrequests are not touched (Cloudflare records them itself). *(0.14+; before that, native mode emitted no `gen_ai.*` at all.)*
|
|
279
|
+
- **Direct exporter** (`nativeTracing: "direct"`) — the attributes land on the outbound CLIENT span.
|
|
280
|
+
|
|
281
|
+
**Detected providers (direct hosts):**
|
|
218
282
|
- **OpenAI** — `api.openai.com`
|
|
219
283
|
- **Anthropic** — `api.anthropic.com`
|
|
220
|
-
- **
|
|
284
|
+
- **Mistral** — `api.mistral.ai`
|
|
221
285
|
- **Google** — `generativelanguage.googleapis.com`
|
|
286
|
+
- **Cloudflare AI Gateway** — any host ending in `gateway.ai.cloudflare.com`
|
|
287
|
+
|
|
288
|
+
**AI gateways / proxies (`ai.gateways`):** if you call providers through a gateway such as [Wafer](https://wafersecurity.ai) instead of the provider host, tell the SDK how to read the provider off the URL. Each entry names the gateway host (exact or any subdomain) and a path pattern whose `:provider` segment carries the provider token — one of `openai`, `anthropic`, `google`, `gemini` (→ `google`), `mistral`, `cloudflare`, `workers-ai` (→ `cloudflare`). Literal segments must match; other `:params` match any segment; the provider's own API path may follow. Direct hosts are checked first; a gateway URL whose token is unknown is left untraced.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
export default instrument(worker, {
|
|
292
|
+
service: "tp-api",
|
|
293
|
+
ai: {
|
|
294
|
+
gateways: [{ hostSuffix: "wafersecurity.ai", pathPattern: "/p/:tenant/:provider" }],
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
// https://wafersecurity.ai/p/talkpod/mistral/v1/chat/completions
|
|
298
|
+
// → gen_ai.system = "mistral", heystack.ai.gateway = "wafersecurity.ai"
|
|
299
|
+
// https://wafersecurity.ai/p/talkpod/gemini/v1beta/models/gemini-2.5-flash:streamGenerateContent
|
|
300
|
+
// → gen_ai.system = "google", heystack.ai.gateway = "wafersecurity.ai"
|
|
301
|
+
```
|
|
222
302
|
|
|
223
303
|
**Attributes captured automatically (metadata, always on):**
|
|
224
304
|
|
|
225
305
|
| Attribute | Description |
|
|
226
306
|
| --- | --- |
|
|
227
|
-
| `gen_ai.system` | Provider name (`openai`, `anthropic`, `
|
|
307
|
+
| `gen_ai.system` | Provider name (`openai`, `anthropic`, `mistral`, `google`, `cloudflare`) |
|
|
308
|
+
| `gen_ai.operation.name` | `chat`, `embeddings` or `text_completion`, inferred from the request path |
|
|
309
|
+
| `heystack.ai.gateway` | Gateway hostname — only when the provider was resolved via `ai.gateways` |
|
|
228
310
|
| `gen_ai.request.model` | Model from the request body (`body.model`) |
|
|
229
311
|
| `gen_ai.request.max_tokens` | Max tokens from the request (`max_tokens` or `max_completion_tokens`) |
|
|
230
312
|
| `gen_ai.request.temperature` | Temperature from the request |
|
|
231
|
-
| `gen_ai.response.model` | Model from the response
|
|
232
|
-
| `gen_ai.response.id` | Response ID |
|
|
233
|
-
| `gen_ai.usage.input_tokens` | Input/prompt tokens
|
|
234
|
-
| `gen_ai.usage.output_tokens` | Output/completion tokens
|
|
235
|
-
| `gen_ai.response.finish_reason` | Stop reason (`stop`, `end_turn`, etc.) |
|
|
313
|
+
| `gen_ai.response.model` | Model from the response (`model`, or Google's `modelVersion`) |
|
|
314
|
+
| `gen_ai.response.id` | Response ID (`id`, or Google's `responseId`) |
|
|
315
|
+
| `gen_ai.usage.input_tokens` | Input/prompt tokens (`usage.prompt_tokens`, `usage.input_tokens`, or `usageMetadata.promptTokenCount`) |
|
|
316
|
+
| `gen_ai.usage.output_tokens` | Output/completion tokens (`usage.completion_tokens`, `usage.output_tokens`, or `usageMetadata.candidatesTokenCount`) |
|
|
317
|
+
| `gen_ai.response.finish_reason` | Stop reason (`stop`, `end_turn`, `STOP`, etc.) |
|
|
236
318
|
|
|
237
|
-
Streaming responses (`text/event-stream`)
|
|
319
|
+
**Streaming responses (`text/event-stream`)** are enriched too (0.14+). The body is `tee()`d: your handler gets one branch untouched (byte-for-byte, no added latency); the SDK reads the other **without buffering content** — each `data:` line's JSON is inspected only for `usage` / `usageMetadata`, model, id and finish reason, then discarded. That covers Mistral and OpenAI (usage in the final chunk — for OpenAI send `stream_options: { include_usage: true }`), Anthropic (`message_start` input tokens + `message_delta` output tokens) and Google (`usageMetadata` on the last chunk). The span stays open until the stream closes (or errors), so its duration is the true time-to-last-token. Lines over 64 KB are skipped unparsed; a scanner failure never throws into your stream. Content is never captured from streams, even with `captureContent: true`.
|
|
238
320
|
|
|
239
321
|
**Content capture (opt-in, strongly recommended for AI-app RCA):**
|
|
240
322
|
|
|
@@ -263,8 +345,9 @@ When `captureContent` is enabled, two additional attributes appear on the CLIENT
|
|
|
263
345
|
|
|
264
346
|
**Body safety guarantees:**
|
|
265
347
|
- Request body: only read when it is already a plain `string` (the common LLM SDK case). Non-string bodies (streams, FormData, etc.) are never touched.
|
|
266
|
-
- Response body: read only from `response.clone()
|
|
267
|
-
- Parse failures or unexpected shapes degrade silently to the plain
|
|
348
|
+
- Response body: JSON is read only from `response.clone()`; a stream is `tee()`d and your branch is handed back immediately. Either way the response you receive is intact and your handler can call `.json()` / `.text()` / `.body.getReader()` normally.
|
|
349
|
+
- Parse failures or unexpected shapes degrade silently to the plain span — no errors are thrown.
|
|
350
|
+
- Under native tracing the wrapper needs the request's native tracer in the active context, which propagates across `await`s only with `nodejs_compat` (AsyncLocalStorage). Without it an LLM call made after an `await` passes through untraced — the same caveat as `withSpan`. Use `withCloudflareSpan(ctx, …)` + `genAiResponseAttrs` for a manual span if you cannot enable `nodejs_compat`.
|
|
268
351
|
- For a detected provider with a JSON response, the cloned body is parsed before the `fetch()` promise resolves to your handler (the span must close with usage attributes). This adds negligible latency to an already-slow LLM call.
|
|
269
352
|
- The response size guard (512 KB) only applies when the response carries a `content-length` header; a chunked JSON response without one is still read. The known providers bound this via `max_tokens`, so it is safe in practice.
|
|
270
353
|
|
|
@@ -288,10 +371,35 @@ const data = await withSpan("call-llm", async () => {
|
|
|
288
371
|
});
|
|
289
372
|
```
|
|
290
373
|
|
|
291
|
-
`withSpan(name, attrs?, fn)` — runs `fn` inside a new child span parented to the currently-active span. The span is started before `fn` and ended in `finally`; exceptions are recorded on the span and re-thrown. The `fn` receives the live `Span` as its argument.
|
|
374
|
+
`withSpan(name, attrs?, fn)` — runs `fn` inside a new child span parented to the currently-active span. The span is started before `fn` and ended in `finally`; exceptions are recorded on the span and re-thrown. The `fn` receives the live `Span` as its argument, so you can `span.setAttribute(s)` after the work has produced a result. Under Cloudflare native tracing it joins the native tree; otherwise it is an OTel child span.
|
|
292
375
|
|
|
293
376
|
`addEvent(name, attrs?)` — adds a named event to the currently-active span. No-op when no span is active.
|
|
294
377
|
|
|
378
|
+
#### LLM calls the auto-wrapper can't see (`env.AI.run`, SDKs with their own transport)
|
|
379
|
+
|
|
380
|
+
The automatic `gen_ai.*` enrichment hooks `fetch`. Anything that doesn't go through `fetch` — Workers AI (`env.AI.run`), a binding, an SDK with a custom transport — is invisible to it. `withSpan` plus the two pure helpers `genAiRequestAttrs(provider, requestBody)` and `genAiResponseAttrs(provider, result)` (both exported from `/workers`) is the supported path; it works identically under native tracing and the direct exporter. Name the span `<operation> <provider>` so it lines up with the automatic ones:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { instrument, withSpan, genAiRequestAttrs, genAiResponseAttrs } from "@heystack/otel/workers";
|
|
384
|
+
|
|
385
|
+
async function chat(env: Env, prompt: string) {
|
|
386
|
+
const model = "@cf/meta/llama-3.1-8b-instruct";
|
|
387
|
+
return withSpan(
|
|
388
|
+
"chat cloudflare",
|
|
389
|
+
{ ...genAiRequestAttrs("cloudflare", { model, max_tokens: 256 }), "gen_ai.operation.name": "chat" },
|
|
390
|
+
async (span) => {
|
|
391
|
+
const result = await env.AI.run(model, { prompt, max_tokens: 256 });
|
|
392
|
+
// Workers AI returns `usage: { prompt_tokens, completion_tokens }` for most text models;
|
|
393
|
+
// genAiResponseAttrs maps it to gen_ai.usage.input_tokens / output_tokens (no-op if absent).
|
|
394
|
+
span.setAttributes(genAiResponseAttrs("cloudflare", result));
|
|
395
|
+
return result;
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`genAiRequestAttrs` sets `gen_ai.system` + `gen_ai.request.{model,max_tokens,temperature}`; `genAiResponseAttrs` understands OpenAI/Mistral, Anthropic, Google and Workers AI response shapes and returns `gen_ai.response.{model,id,finish_reason}` + `gen_ai.usage.*`. Neither ever throws or captures prompt/completion text. (If `instrumentBindings` is on under the direct exporter, `env.AI.run` is already traced automatically — use the manual form under native tracing or for other call paths.)
|
|
402
|
+
|
|
295
403
|
### Why a ContextManager
|
|
296
404
|
|
|
297
405
|
`context.with(...)` in OpenTelemetry is a **no-op unless a ContextManager is registered** with the global API. Without one, `suppressTracing()` — the primary defence against the self-trace feedback loop — silently does nothing in production. As of **0.3.2** the SDK registers a ContextManager exactly once at init. With `AsyncLocalStorage` (workerd under `nodejs_compat`, Node) you also get **cross-`await` parent→child span linking** and **per-request context isolation** — concurrent requests no longer share or clobber the active span.
|
|
@@ -307,9 +415,9 @@ inbound W3C `traceparent` (so a client request and the server handler share one
|
|
|
307
415
|
trace) and returns a `traceparent` response header — plus
|
|
308
416
|
`Access-Control-Expose-Headers: traceparent` so browser clients can read it.
|
|
309
417
|
|
|
310
|
-
### Durable Objects
|
|
418
|
+
### Durable Objects in direct fallback mode
|
|
311
419
|
|
|
312
|
-
`instrument()` wraps the keys of the **default-export handler object** (`fetch`/`queue`/`scheduled`/… )
|
|
420
|
+
Cloudflare native tracing covers Durable Objects and their platform operations. The limitation below applies only to the direct exporter fallback: `instrument()` wraps the keys of the **default-export handler object** (`fetch`/`queue`/`scheduled`/… ), while Durable Objects are separate named class exports.
|
|
313
421
|
|
|
314
422
|
To trace a Durable Object, instrument it manually with the global tracer (which `instrument()` / `initHeystackWorkers()` already registered) and flush per invocation:
|
|
315
423
|
|
|
@@ -484,6 +592,8 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
484
592
|
|
|
485
593
|
## Migration / versioning
|
|
486
594
|
|
|
595
|
+
- **`0.14.0`** — **gen_ai enrichment under Cloudflare native tracing, Mistral + AI-gateway detection, streaming usage; OpenTelemetry JS SDK 2.x.** (1) `/workers`: under `nativeTracing: "auto" | "native"` `instrument()` never installed the fetch wrapper, so native-mode Workers emitted **no** `gen_ai.*` telemetry. It now installs a lean LLM-only wrapper that opens a native `<operation> <provider>` span (e.g. `chat mistral`) with request/response `gen_ai.*` for detected providers and passes every other subrequest through untouched. (2) New provider **Mistral** (`api.mistral.ai`) and a new `ai.gateways` option (`[{ hostSuffix, pathPattern }]`, e.g. `{ hostSuffix: "wafersecurity.ai", pathPattern: "/p/:tenant/:provider" }`) that resolves the provider from an AI-gateway URL and stamps `heystack.ai.gateway`; `gemini` → `google`, `workers-ai` → `cloudflare`. New `gen_ai.operation.name`. (3) **Streaming (`text/event-stream`) responses now yield usage**: the body is `tee()`d and scanned line-by-line for `usage`/`usageMetadata`, model, id and finish reason only (never content); the span stays open until the stream closes. Google (`usageMetadata`) and Mistral response shapes added to the JSON path too. (4) New `/workers` exports `genAiRequestAttrs` / `genAiResponseAttrs` for manual `withSpan` spans around `env.AI.run` or any non-`fetch` LLM call. Native `error.type` now keeps the exception class instead of being overwritten with `error`. (5) All `@opentelemetry/*` dependencies move to the 2.x SDK line (`core`/`sdk-trace-base`/`resources` ^2.11, `sdk-node`/`exporter-trace-otlp-http` ^0.222, `auto-instrumentations-node` ^0.80, `semantic-conventions` ^1.43), clearing the downstream Dependabot alerts the 1.x line surfaced through this package (GHSA-8988-4f7v-96qf in `@opentelemetry/core`, GHSA-q7rr-3cgh-j5r3 in `sdk-node` / `auto-instrumentations-node` / `exporter-prometheus`, GHSA-45rx-2jwx-cxfr in `propagator-jaeger`). **Node ≥ 18.19 (or ≥ 20.6) is now required on `/node` and `/next`** — the 2.x SDK dropped Node 14/16. If you pass your own `instrumentations` array to `initHeystack`, upgrade those `@opentelemetry/instrumentation-*` packages to a release that peers on `@opentelemetry/core@^2` (the auto-instrumentations 0.80 set already does). `/workers` is still WinterCG-safe (no `node:*` imports). No other API changes; all new options are optional.
|
|
596
|
+
- **`0.13.0`** — **Cloudflare-native tracing and complete Workers platform coverage.** On runtimes exposing `ctx.tracing`, `/workers` now joins Cloudflare's authoritative invocation trace instead of creating and exporting a duplicate tree. `nativeTracing: "auto"` is the default; `withCloudflareSpan(ctx, ...)` adds async-safe business spans, while fetch, queue, and scheduled handlers are enriched in the native tree. The new `/workers/agents` entry maps Cloudflare Agents observability callbacks and Tail Worker diagnostics to structured, redacted telemetry. Direct OTLP remains available with `nativeTracing: "direct"` for runtimes without the native API. Heystack ingest now preserves full OTLP trace/log fidelity, accepts OTLP metrics, and can import Workers Analytics metrics through the app-scoped Cloudflare connector. Native mode needs no Worker API-key secret and must not be combined with direct export.
|
|
487
597
|
- **`0.11.0`** — **`/web`: browser error + console collection, in-app bug reports, and seekable replay keyframes.** `instrumentWeb` now captures uncaught errors (`window.onerror` + `unhandledrejection`) and `console.error` as OTLP logs by default (`event.name=browser.error` / `browser.console`, with `exception.*` semconv, `url.full`, `session_id`, and the active trace/span ids when `tracing` is on). New options: `errors?: boolean` (default `true`), `captureConsole?: 'error' | 'warn' | false` (default `'error'`). New **`reportBug({ message, email?, context? })`** API — a headless in-app bug reporter that POSTs to `/v1/bug-reports` and auto-attaches the URL, user agent, `session_id`, active `trace_id`, `release`/`build`, and the last ≤20 captured browser errors (also emitted as a `user.bug_report` log); reports triage in the console **Bugs** tab. The `version` / `build` options are now stamped as `service.version` / `service.build` on exported browser logs (release attribution). Console capture is recursion-guarded + rate-capped (no self-export loop, no flood). The recorder also checkpoints a keyframe (~60s) so the console player seeks from the nearest snapshot instead of loading the whole session. No breaking changes; all new options are optional.
|
|
488
598
|
- **`0.10.0`** — **release / commit attribution (`version` + `build`) on every runtime entry.** New optional options: `version` → the `service.version` resource attribute (a release identifier such as `1.4.2` or a git tag), `build` → the `service.build` resource attribute (the commit SHA). They power **release health**, **suspect release**, and **suspect commit** in the console — attributing a regression to the version/commit that introduced it (suspect-commit deep-links to the commit when a repo URL is configured for the app). Wired on `/node`, `/next` (threaded to whichever runtime runs), and `/workers`; accepted on `/web` for API symmetry. `/node` also reads `HEYSTACK_SERVICE_VERSION` / `OTEL_SERVICE_VERSION` and `HEYSTACK_SERVICE_BUILD` as env fallbacks. Both options are optional and only emitted when non-empty. No breaking changes. See [Release & commit attribution](#release--commit-attribution-version--build).
|
|
489
599
|
- **`0.9.2`** — **`/workers`: `instrument()` no longer breaks WebSocket / Durable Object upgrades.** The fetch wrapper rebuilt every response with `new Response(...)`, which **drops the `webSocket` property** on a `101 Switching Protocols` upgrade — so every WebSocket connection on an instrumented Worker failed *whenever tracing was active* (an API key set), while passing in dev/staging where `instrument()` is a passthrough. `instrument()` now detects an upgrade (`response.webSocket` present, or `status === 101`) and returns the **original response untouched** (span closed immediately; no `traceparent` header injected — headers on a 101 aren't delivered anyway). Fixes failed `wss://` connections behind `routeAgentRequest` / any `new WebSocketPair()` handler. No API change; upgrade and redeploy.
|
package/dist/llm-enrich.d.ts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
|
-
export type LLMProvider = "openai" | "anthropic" | "cloudflare" | "google";
|
|
1
|
+
export type LLMProvider = "openai" | "anthropic" | "cloudflare" | "google" | "mistral";
|
|
2
2
|
/** Return the provider for a known LLM host, else null. Host is lower-case, no port. */
|
|
3
3
|
export declare function detectLLMProvider(host: string): LLMProvider | null;
|
|
4
|
+
export interface LLMGatewayConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Gateway hostname. Matches the exact host or any subdomain of it
|
|
7
|
+
* (`wafersecurity.ai` matches `wafersecurity.ai` and `gw.wafersecurity.ai`,
|
|
8
|
+
* never `notwafersecurity.ai`). A leading dot is tolerated.
|
|
9
|
+
*/
|
|
10
|
+
hostSuffix: string;
|
|
11
|
+
/**
|
|
12
|
+
* Path pattern whose `:provider` segment names the upstream provider, e.g.
|
|
13
|
+
* `/p/:tenant/:provider`. Literal segments must match exactly; other `:param`
|
|
14
|
+
* segments match any single segment; the request path may carry further
|
|
15
|
+
* segments after the pattern (the provider's own API path).
|
|
16
|
+
*/
|
|
17
|
+
pathPattern: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ResolvedLLMProvider {
|
|
20
|
+
provider: LLMProvider;
|
|
21
|
+
/** Set (to the gateway hostname) when the provider was resolved via a gateway. */
|
|
22
|
+
gateway?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Map a gateway path token to a provider. `gemini` → google, `workers-ai` → cloudflare. */
|
|
25
|
+
export declare function providerFromToken(token: string): LLMProvider | null;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the LLM provider for an outbound URL: direct host table first, then
|
|
28
|
+
* each configured gateway pattern. Returns null when neither matches. Pure and
|
|
29
|
+
* exception-free (a malformed gateway entry is skipped, never thrown).
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveLLMProvider(url: URL, gateways?: readonly LLMGatewayConfig[]): ResolvedLLMProvider | null;
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort `gen_ai.operation.name` from the request path. Chat-style
|
|
34
|
+
* endpoints (OpenAI/Mistral `chat/completions`, Anthropic `messages`, Google
|
|
35
|
+
* `*generateContent`) → `chat`; `embeddings` → `embeddings`; bare/fim
|
|
36
|
+
* `completions` → `text_completion`; anything else defaults to `chat`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function llmOperationName(pathname: string): "chat" | "embeddings" | "text_completion";
|
|
4
39
|
export interface LLMRequestAttrs {
|
|
5
40
|
[k: string]: string | number;
|
|
6
41
|
}
|
|
@@ -8,6 +43,33 @@ export interface LLMRequestAttrs {
|
|
|
8
43
|
export declare function llmRequestAttrs(provider: LLMProvider, body: unknown): LLMRequestAttrs;
|
|
9
44
|
/** Best-effort gen_ai.response.* + usage from a parsed JSON response body. Never throws. */
|
|
10
45
|
export declare function llmResponseAttrs(provider: LLMProvider, body: unknown): Record<string, string | number>;
|
|
46
|
+
/** Lines longer than this are skipped unparsed (a bound on per-chunk JSON.parse). */
|
|
47
|
+
export declare const SSE_MAX_LINE_BYTES: number;
|
|
48
|
+
/**
|
|
49
|
+
* Metadata attrs from ONE parsed stream chunk. Shares the response mapper and
|
|
50
|
+
* adds the Anthropic envelope (`message.{id,model,usage}` on `message_start`,
|
|
51
|
+
* `delta.stop_reason` on `message_delta`). Never throws.
|
|
52
|
+
*/
|
|
53
|
+
export declare function llmStreamChunkAttrs(provider: LLMProvider, chunk: unknown): Record<string, string | number>;
|
|
54
|
+
export interface SseUsageScanner {
|
|
55
|
+
/** Feed decoded text (any chunk boundary). Never throws. */
|
|
56
|
+
feed(text: string): void;
|
|
57
|
+
/** Flush a trailing partial line and return the accumulated attrs. */
|
|
58
|
+
result(): Record<string, string | number>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Incremental SSE scanner: buffers at most one partial line, parses complete
|
|
62
|
+
* `data:` lines for usage/model/id/finish_reason and drops everything else
|
|
63
|
+
* immediately. Lines over `SSE_MAX_LINE_BYTES` are skipped without parsing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createSseUsageScanner(provider: LLMProvider): SseUsageScanner;
|
|
66
|
+
/**
|
|
67
|
+
* Drain a (teed) SSE body through the scanner and resolve with the accumulated
|
|
68
|
+
* metadata attrs when the stream closes. Resolves — never rejects — on a stream
|
|
69
|
+
* error, returning whatever was scanned so far. Reads chunk-by-chunk with a
|
|
70
|
+
* streaming `TextDecoder`, so the body is never buffered.
|
|
71
|
+
*/
|
|
72
|
+
export declare function scanSseStreamForUsage(stream: ReadableStream<Uint8Array>, provider: LLMProvider): Promise<Record<string, string | number>>;
|
|
11
73
|
/** Content attrs, ONLY when captureContent is on. Size-capped per value, redactor applied. */
|
|
12
74
|
export declare function llmContentAttrs(provider: LLMProvider, reqBody: unknown, resBody: unknown, opts: {
|
|
13
75
|
redact?: (text: string) => string;
|