@heystack/otel 0.11.1 → 0.13.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 +72 -6
- package/dist/web.d.ts +28 -0
- package/dist/web.js +25 -2
- package/dist/workers-agents.d.ts +49 -0
- package/dist/workers-agents.js +110 -0
- package/dist/workers.d.ts +30 -0
- package/dist/workers.js +170 -1
- package/package.json +5 -1
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.13.0`.** 0.13 makes Cloudflare's native trace tree authoritative, adds async-safe native custom spans, queue/cron coverage, and the Agents SDK adapter. 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 patch `fetch`, does not proxy bindings, and does not require `HEYSTACK_API_KEY` in the Worker.
|
|
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,6 +208,7 @@ 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`. |
|
|
@@ -307,9 +368,9 @@ inbound W3C `traceparent` (so a client request and the server handler share one
|
|
|
307
368
|
trace) and returns a `traceparent` response header — plus
|
|
308
369
|
`Access-Control-Expose-Headers: traceparent` so browser clients can read it.
|
|
309
370
|
|
|
310
|
-
### Durable Objects
|
|
371
|
+
### Durable Objects in direct fallback mode
|
|
311
372
|
|
|
312
|
-
`instrument()` wraps the keys of the **default-export handler object** (`fetch`/`queue`/`scheduled`/… )
|
|
373
|
+
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
374
|
|
|
314
375
|
To trace a Durable Object, instrument it manually with the global tracer (which `instrument()` / `initHeystackWorkers()` already registered) and flush per invocation:
|
|
315
376
|
|
|
@@ -400,6 +461,8 @@ await instrumentWeb({
|
|
|
400
461
|
|
|
401
462
|
Otherwise it's **cost-aware and safe by design**: off unless you opt in; head-sampled (an unsampled request still propagates `traceparent` with the sampled flag cleared, so the backend makes the same keep/drop decision — no orphaned server spans); and the exporter posts through the *original* `fetch`, never tracing its own upload (no self-export loop). Spans post to `/v1/traces` cross-origin with no CORS setup on your side.
|
|
402
463
|
|
|
464
|
+
**Remote kill-switch (safety valve).** Browser tracing can also be disabled **from the server**, without an app redeploy — a way to stop it fleet-wide if it ever misbehaves in the wild. It is **fail-open**: if the server config is unreachable or omits the flag, tracing stays on (absent flag = enabled), so the switch can only ever *disable*, never accidentally turn a working app off.
|
|
465
|
+
|
|
403
466
|
### Browser error & console collection
|
|
404
467
|
|
|
405
468
|
`instrumentWeb` captures **uncaught browser errors** out of the box — no extra setup. Any `window.onerror` / unhandled promise rejection becomes an OTLP **log** (`event.name=browser.error`, `ERROR` severity) carrying the OTel `exception.type` / `exception.message` / `exception.stacktrace` semconv attributes, the page `url.full`, the `session_id` (so it correlates to the session replay), and — when `tracing` is on — the active `trace_id` / `span_id`. `console.error` is captured too by default.
|
|
@@ -417,6 +480,8 @@ await instrumentWeb({
|
|
|
417
480
|
|
|
418
481
|
It's the **same cost-safe design** as tracing/replay: logs POST to `/v1/logs` through the *original* `fetch` (captured before any patching) so the exporter never traces its own upload — no self-export loop. Console capture is **recursion-guarded** (anything logged on the export path is never re-captured) and **rate-capped** (max ~60 records/minute; the overflow is dropped and counted, with one summary log emitted when the cap lifts) so a runaway `console.error` in a render loop can't flood ingest. Errors show up in the console **Logs** tab and correlate to their session replay and trace.
|
|
419
482
|
|
|
483
|
+
Like tracing, error / console capture has a **remote kill-switch**: it can be disabled **from the server** without an app redeploy, and is **fail-open** (if the server config is unreachable or omits the flag, capture stays on — absent flag = enabled).
|
|
484
|
+
|
|
420
485
|
### In-app bug reports (`reportBug`)
|
|
421
486
|
|
|
422
487
|
Let users report a bug from inside your app. `reportBug` is a headless API — **no widget, you own the UX** — that files a structured report and auto-attaches the context a triager needs: the current URL, user agent, replay `session_id`, active `trace_id`, `release` / `build`, and the **last few captured browser errors** (so you see what was going wrong on the page right before the report). The report also appears on the Logs timeline (`event.name=user.bug_report`).
|
|
@@ -480,6 +545,7 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
480
545
|
|
|
481
546
|
## Migration / versioning
|
|
482
547
|
|
|
548
|
+
- **`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.
|
|
483
549
|
- **`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.
|
|
484
550
|
- **`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).
|
|
485
551
|
- **`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/web.d.ts
CHANGED
|
@@ -17,6 +17,19 @@ export interface RecorderConfig {
|
|
|
17
17
|
enabled: boolean;
|
|
18
18
|
sample_rate: number;
|
|
19
19
|
masking_mode: "strict" | "relaxed";
|
|
20
|
+
/**
|
|
21
|
+
* Server-side kill-switch for browser distributed tracing. Absent/`true` = enabled
|
|
22
|
+
* (**fail-open**); set `false` server-side to remotely disable browser tracing
|
|
23
|
+
* WITHOUT an app redeploy — the safety valve for the 2026-07-03 cross-origin
|
|
24
|
+
* `traceparent` outage. See `tracingActive`.
|
|
25
|
+
*/
|
|
26
|
+
tracing_enabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Server-side kill-switch for browser error / console capture. Absent/`true` =
|
|
29
|
+
* enabled (**fail-open**); set `false` server-side to remotely disable it without
|
|
30
|
+
* an app redeploy. See `errorCaptureActive`.
|
|
31
|
+
*/
|
|
32
|
+
errors_enabled?: boolean;
|
|
20
33
|
}
|
|
21
34
|
/** Pure: decide once per session whether to record. rng defaults to Math.random. */
|
|
22
35
|
export declare function shouldRecord(cfg: RecorderConfig, rng?: () => number): boolean;
|
|
@@ -116,6 +129,21 @@ export interface InstrumentWebOptions {
|
|
|
116
129
|
*/
|
|
117
130
|
captureConsole?: "error" | "warn" | false;
|
|
118
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Pure: is browser distributed tracing active for this run? Requires BOTH the app
|
|
134
|
+
* to have opted in (`opts.tracing`) AND the server-side kill-switch to not be off
|
|
135
|
+
* (`cfg.tracing_enabled`). **Fail-open**: an absent/`true` flag counts as enabled, so
|
|
136
|
+
* an old or unreachable `/v1/replay/config` never silently disables tracing. Split out
|
|
137
|
+
* as a tiny exported predicate so the gating decision is unit-testable without booting
|
|
138
|
+
* rrweb (which `instrumentWeb` imports). See `RecorderConfig.tracing_enabled`.
|
|
139
|
+
*/
|
|
140
|
+
export declare function tracingActive(opts: Pick<InstrumentWebOptions, "tracing">, cfg: Pick<RecorderConfig, "tracing_enabled">): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Pure: is browser error / console capture active for this run? On by default
|
|
143
|
+
* (`opts.errors !== false`) AND gated by the server-side kill-switch
|
|
144
|
+
* (`cfg.errors_enabled`). **Fail-open**: an absent/`true` flag counts as enabled.
|
|
145
|
+
*/
|
|
146
|
+
export declare function errorCaptureActive(opts: Pick<InstrumentWebOptions, "errors">, cfg: Pick<RecorderConfig, "errors_enabled">): boolean;
|
|
119
147
|
/** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
|
|
120
148
|
* Returns a stop() function. Safe to call in any browser; no-ops on the server. */
|
|
121
149
|
export declare function instrumentWeb(opts: InstrumentWebOptions): Promise<() => void>;
|
package/dist/web.js
CHANGED
|
@@ -81,6 +81,25 @@ export class ReplayTransport {
|
|
|
81
81
|
}
|
|
82
82
|
const DEFAULT_FLUSH_MS = 5000;
|
|
83
83
|
const DEFAULT_FLUSH_EVENTS = 200;
|
|
84
|
+
/**
|
|
85
|
+
* Pure: is browser distributed tracing active for this run? Requires BOTH the app
|
|
86
|
+
* to have opted in (`opts.tracing`) AND the server-side kill-switch to not be off
|
|
87
|
+
* (`cfg.tracing_enabled`). **Fail-open**: an absent/`true` flag counts as enabled, so
|
|
88
|
+
* an old or unreachable `/v1/replay/config` never silently disables tracing. Split out
|
|
89
|
+
* as a tiny exported predicate so the gating decision is unit-testable without booting
|
|
90
|
+
* rrweb (which `instrumentWeb` imports). See `RecorderConfig.tracing_enabled`.
|
|
91
|
+
*/
|
|
92
|
+
export function tracingActive(opts, cfg) {
|
|
93
|
+
return !!opts.tracing && cfg.tracing_enabled !== false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Pure: is browser error / console capture active for this run? On by default
|
|
97
|
+
* (`opts.errors !== false`) AND gated by the server-side kill-switch
|
|
98
|
+
* (`cfg.errors_enabled`). **Fail-open**: an absent/`true` flag counts as enabled.
|
|
99
|
+
*/
|
|
100
|
+
export function errorCaptureActive(opts, cfg) {
|
|
101
|
+
return opts.errors !== false && cfg.errors_enabled !== false;
|
|
102
|
+
}
|
|
84
103
|
/** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
|
|
85
104
|
* Returns a stop() function. Safe to call in any browser; no-ops on the server. */
|
|
86
105
|
export async function instrumentWeb(opts) {
|
|
@@ -113,7 +132,9 @@ export async function instrumentWeb(opts) {
|
|
|
113
132
|
// a real CLIENT span per outbound fetch + propagates W3C context to the backend.
|
|
114
133
|
let stopTracing = () => { };
|
|
115
134
|
let fetchPatched = false;
|
|
116
|
-
|
|
135
|
+
// Gated by the server kill-switch (cfg.tracing_enabled) as well as the app opt-in —
|
|
136
|
+
// fail-open, so an unreachable/old config can't disable tracing. See tracingActive.
|
|
137
|
+
if (tracingActive(opts, cfg)) {
|
|
117
138
|
const exporter = new BrowserTraceExporter({
|
|
118
139
|
endpoint, apiKey: opts.apiKey, service: opts.service, fetchImpl: originalFetch,
|
|
119
140
|
resourceAttributes: releaseAttrs,
|
|
@@ -157,7 +178,9 @@ export async function instrumentWeb(opts) {
|
|
|
157
178
|
// 2b. Browser error / console log collection — ON by default, INDEPENDENT of both
|
|
158
179
|
// tracing and replay sampling. Exports OTLP logs to /v1/logs via the original fetch.
|
|
159
180
|
let stopErrors = () => { };
|
|
160
|
-
|
|
181
|
+
// Gated by the server kill-switch (cfg.errors_enabled) as well as the opts.errors
|
|
182
|
+
// default — fail-open. See errorCaptureActive.
|
|
183
|
+
if (errorCaptureActive(opts, cfg)) {
|
|
161
184
|
const unpatchErrors = startErrorCapture({
|
|
162
185
|
exporter: logsExporter,
|
|
163
186
|
sessionId,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Span } from "@opentelemetry/api";
|
|
2
|
+
import { type CloudflareNativeTracing } from "./workers";
|
|
3
|
+
/** Structural equivalent of the Agents SDK event; no `agents` peer required. */
|
|
4
|
+
export interface CloudflareAgentEvent {
|
|
5
|
+
type: string;
|
|
6
|
+
agent?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
payload?: Record<string, unknown>;
|
|
9
|
+
timestamp?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface CloudflareAgentsObservability {
|
|
13
|
+
emit(event: CloudflareAgentEvent): void;
|
|
14
|
+
}
|
|
15
|
+
export interface CloudflareAgentsObservabilityOptions {
|
|
16
|
+
/** Static fields added to every structured event and active-span event. */
|
|
17
|
+
attributes?: Record<string, string | number | boolean>;
|
|
18
|
+
/** Allow-list event types. By default every Agents SDK event is retained. */
|
|
19
|
+
include?: readonly string[];
|
|
20
|
+
/** Additional payload keys to redact. Common secret keys are always redacted. */
|
|
21
|
+
redactKeys?: readonly string[];
|
|
22
|
+
/** Override the structured-log sink (useful for tests/custom routing). */
|
|
23
|
+
logger?: (level: "info" | "error", value: Record<string, unknown>) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Agents SDK `Observability` implementation. It emits one structured log for
|
|
27
|
+
* every lifecycle event and mirrors safe scalar metadata onto the active span.
|
|
28
|
+
* Native Workers log export then preserves trace correlation without an extra
|
|
29
|
+
* network request in the Agent/Durable Object request path.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createCloudflareAgentsObservability(options?: CloudflareAgentsObservabilityOptions): CloudflareAgentsObservability;
|
|
32
|
+
export interface CloudflareDiagnosticsMessage {
|
|
33
|
+
timestamp?: number;
|
|
34
|
+
channel?: string;
|
|
35
|
+
message?: CloudflareAgentEvent | unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface CloudflareTailEvent {
|
|
38
|
+
diagnosticsChannelEvents?: CloudflareDiagnosticsMessage[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Tail Worker handler for production Agents diagnostics channels. Attach the
|
|
42
|
+
* returned handler as a Tail Consumer; its structured logs are exported by the
|
|
43
|
+
* Tail Worker's own native OTLP logs destination.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createCloudflareAgentsTailHandler(options?: CloudflareAgentsObservabilityOptions): {
|
|
46
|
+
tail(events: CloudflareTailEvent[]): Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
/** Explicit async-safe native custom span for Agent model/tool/RPC work. */
|
|
49
|
+
export declare function withCloudflareAgentSpan<T>(tracing: ExecutionContext | CloudflareNativeTracing, operation: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { trace } from "@opentelemetry/api";
|
|
2
|
+
import { withCloudflareSpan, } from "./workers";
|
|
3
|
+
const SENSITIVE_KEY = /(?:authorization|cookie|password|secret|token|api[_-]?key|prompt|completion|content|message)/i;
|
|
4
|
+
const ERROR_EVENT = /(?::error|:failed|:stalled|:interrupted|:exhausted)$/;
|
|
5
|
+
function sanitize(value, extraKeys, depth = 0) {
|
|
6
|
+
if (depth > 5)
|
|
7
|
+
return "[truncated]";
|
|
8
|
+
if (value === null || typeof value === "boolean" || typeof value === "number")
|
|
9
|
+
return value;
|
|
10
|
+
if (typeof value === "string")
|
|
11
|
+
return value.length > 2_000 ? `${value.slice(0, 2_000)}…` : value;
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return value.slice(0, 50).map((item) => sanitize(item, extraKeys, depth + 1));
|
|
14
|
+
if (typeof value === "object") {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [key, item] of Object.entries(value).slice(0, 100)) {
|
|
17
|
+
out[key] = SENSITIVE_KEY.test(key) || extraKeys.has(key) ? "[redacted]" : sanitize(item, extraKeys, depth + 1);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
function eventAttributes(event) {
|
|
24
|
+
const attrs = { "cloudflare.agent.event.type": event.type };
|
|
25
|
+
if (event.agent)
|
|
26
|
+
attrs["cloudflare.agent.class"] = event.agent;
|
|
27
|
+
if (event.name)
|
|
28
|
+
attrs["cloudflare.agent.name"] = event.name;
|
|
29
|
+
if (event.timestamp)
|
|
30
|
+
attrs["cloudflare.agent.event.timestamp_ms"] = event.timestamp;
|
|
31
|
+
for (const [key, value] of Object.entries(event.payload ?? {})) {
|
|
32
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
33
|
+
if (!SENSITIVE_KEY.test(key))
|
|
34
|
+
attrs[`cloudflare.agent.${key}`] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return attrs;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Agents SDK `Observability` implementation. It emits one structured log for
|
|
41
|
+
* every lifecycle event and mirrors safe scalar metadata onto the active span.
|
|
42
|
+
* Native Workers log export then preserves trace correlation without an extra
|
|
43
|
+
* network request in the Agent/Durable Object request path.
|
|
44
|
+
*/
|
|
45
|
+
export function createCloudflareAgentsObservability(options = {}) {
|
|
46
|
+
const include = options.include ? new Set(options.include) : undefined;
|
|
47
|
+
const redactKeys = new Set(options.redactKeys ?? []);
|
|
48
|
+
const logger = options.logger ?? ((level, value) => {
|
|
49
|
+
if (level === "error")
|
|
50
|
+
console.error(value);
|
|
51
|
+
else
|
|
52
|
+
console.log(value);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
emit(event) {
|
|
56
|
+
if (!event?.type || (include && !include.has(event.type)))
|
|
57
|
+
return;
|
|
58
|
+
const level = ERROR_EVENT.test(event.type) ? "error" : "info";
|
|
59
|
+
const safeEvent = sanitize(event, redactKeys);
|
|
60
|
+
const value = {
|
|
61
|
+
event: "cloudflare.agent",
|
|
62
|
+
...options.attributes,
|
|
63
|
+
...safeEvent,
|
|
64
|
+
severity: level,
|
|
65
|
+
};
|
|
66
|
+
logger(level, value);
|
|
67
|
+
const active = trace.getActiveSpan();
|
|
68
|
+
if (active?.isRecording())
|
|
69
|
+
active.addEvent(`cloudflare.agent.${event.type}`, {
|
|
70
|
+
...options.attributes,
|
|
71
|
+
...eventAttributes(event),
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tail Worker handler for production Agents diagnostics channels. Attach the
|
|
78
|
+
* returned handler as a Tail Consumer; its structured logs are exported by the
|
|
79
|
+
* Tail Worker's own native OTLP logs destination.
|
|
80
|
+
*/
|
|
81
|
+
export function createCloudflareAgentsTailHandler(options = {}) {
|
|
82
|
+
const observability = createCloudflareAgentsObservability(options);
|
|
83
|
+
return {
|
|
84
|
+
async tail(events) {
|
|
85
|
+
for (const event of events) {
|
|
86
|
+
for (const diagnostic of event.diagnosticsChannelEvents ?? []) {
|
|
87
|
+
if (!diagnostic.channel?.startsWith("agents:"))
|
|
88
|
+
continue;
|
|
89
|
+
const message = diagnostic.message;
|
|
90
|
+
if (!message || typeof message !== "object")
|
|
91
|
+
continue;
|
|
92
|
+
const agentEvent = message;
|
|
93
|
+
observability.emit({
|
|
94
|
+
...agentEvent,
|
|
95
|
+
type: agentEvent.type || diagnostic.channel.slice("agents:".length),
|
|
96
|
+
timestamp: agentEvent.timestamp ?? diagnostic.timestamp,
|
|
97
|
+
diagnostics_channel: diagnostic.channel,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Explicit async-safe native custom span for Agent model/tool/RPC work. */
|
|
105
|
+
export function withCloudflareAgentSpan(tracing, operation, attrs, fn) {
|
|
106
|
+
return withCloudflareSpan(tracing, `agent.${operation}`, {
|
|
107
|
+
"cloudflare.agent.operation": operation,
|
|
108
|
+
...attrs,
|
|
109
|
+
}, fn);
|
|
110
|
+
}
|
package/dist/workers.d.ts
CHANGED
|
@@ -171,6 +171,17 @@ export interface WorkersConfig {
|
|
|
171
171
|
* Defaults to `false` (no binding tracing). Consumed by a later task.
|
|
172
172
|
*/
|
|
173
173
|
instrumentBindings?: boolean | string[];
|
|
174
|
+
/**
|
|
175
|
+
* Select the Workers tracing plane. `auto` (default) uses Cloudflare's native
|
|
176
|
+
* tracer when `ctx.tracing` is available and otherwise falls back to direct
|
|
177
|
+
* OTLP export. `native` prefers the platform tracer but still fails open when
|
|
178
|
+
* unavailable. `direct` always uses Heystack's fetch exporter.
|
|
179
|
+
*
|
|
180
|
+
* Native mode deliberately disables Heystack root/fetch/binding
|
|
181
|
+
* instrumentation: Cloudflare already emits those spans, and running both
|
|
182
|
+
* creates duplicate, disconnected trace trees.
|
|
183
|
+
*/
|
|
184
|
+
nativeTracing?: "auto" | "native" | "direct";
|
|
174
185
|
/**
|
|
175
186
|
* Head-sampling configuration. When omitted (or `rate` >= 1), all traces are
|
|
176
187
|
* kept. When `rate` < 1, a deterministic FNV-1a hash of the trace ID decides
|
|
@@ -206,6 +217,17 @@ export interface WorkersConfig {
|
|
|
206
217
|
maxContentChars?: number;
|
|
207
218
|
};
|
|
208
219
|
}
|
|
220
|
+
/** Minimal public surface exposed by Cloudflare's Workers custom-spans API. */
|
|
221
|
+
export interface CloudflareNativeSpan {
|
|
222
|
+
readonly isTraced: boolean;
|
|
223
|
+
setAttribute(key: string, value: string | number | boolean | undefined): void;
|
|
224
|
+
end(): void;
|
|
225
|
+
}
|
|
226
|
+
/** `ExecutionContext.tracing` without importing the `cloudflare:workers` module. */
|
|
227
|
+
export interface CloudflareNativeTracing {
|
|
228
|
+
enterSpan<T>(name: string, callback: (span: CloudflareNativeSpan) => T): T;
|
|
229
|
+
startActiveSpan<T>(name: string, callback: (span: CloudflareNativeSpan) => T): T;
|
|
230
|
+
}
|
|
209
231
|
/**
|
|
210
232
|
* A `BasicTracerProvider` with the underlying `HeystackSpanExporter` attached so
|
|
211
233
|
* flush paths can drain its in-flight fetches directly (the OTel processor's
|
|
@@ -388,6 +410,14 @@ export declare function instrument<H extends WorkerHandler<any>>(handler: H, con
|
|
|
388
410
|
*/
|
|
389
411
|
export declare function withSpan<T>(name: string, fn: (span: Span) => T | Promise<T>): Promise<T>;
|
|
390
412
|
export declare function withSpan<T>(name: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
|
|
413
|
+
/**
|
|
414
|
+
* Create a Cloudflare-native custom span using an explicit ExecutionContext.
|
|
415
|
+
* Prefer this form in code that calls `withSpan` after an `await` on a Worker
|
|
416
|
+
* without `nodejs_compat`: the explicit context cannot be lost across async
|
|
417
|
+
* boundaries. It falls back to the regular OTel tracer outside Cloudflare.
|
|
418
|
+
*/
|
|
419
|
+
export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareNativeTracing, name: string, fn: (span: Span) => T | Promise<T>): Promise<T>;
|
|
420
|
+
export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareNativeTracing, name: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
|
|
391
421
|
/**
|
|
392
422
|
* Add a named event (with optional attributes) to the currently-active span.
|
|
393
423
|
* No-op when no span is active.
|
package/dist/workers.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `@opentelemetry/resources` and the platform `fetch` — all WinterCG-safe — and
|
|
8
8
|
// ships its own OTLP/JSON-over-fetch span exporter so it runs on Workers/Edge
|
|
9
9
|
// where the Node SDK cannot.
|
|
10
|
-
import { context, trace, SpanKind, SpanStatusCode, } from "@opentelemetry/api";
|
|
10
|
+
import { context, createContextKey, INVALID_SPAN_CONTEXT, trace, SpanKind, SpanStatusCode, } from "@opentelemetry/api";
|
|
11
11
|
import { suppressTracing, isTracingSuppressed } from "@opentelemetry/core";
|
|
12
12
|
import { ROOT_CONTEXT } from "@opentelemetry/api";
|
|
13
13
|
import { Resource } from "@opentelemetry/resources";
|
|
@@ -563,6 +563,115 @@ export class HeystackSpanExporter {
|
|
|
563
563
|
await Promise.allSettled([...this.pending]);
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
|
+
const NATIVE_TRACING_CONTEXT_KEY = createContextKey("heystack.cloudflare.native-tracing");
|
|
567
|
+
function nativeTracingFromContext() {
|
|
568
|
+
return context.active().getValue(NATIVE_TRACING_CONTEXT_KEY);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Adapts Cloudflare's deliberately-small Span API to the OTel API shape used by
|
|
572
|
+
* `withSpan`. Unsupported OTel operations degrade to attributes instead of
|
|
573
|
+
* creating a second exporter/provider.
|
|
574
|
+
*/
|
|
575
|
+
class NativeSpanAdapter {
|
|
576
|
+
nativeSpan;
|
|
577
|
+
autoEnd;
|
|
578
|
+
constructor(nativeSpan, autoEnd) {
|
|
579
|
+
this.nativeSpan = nativeSpan;
|
|
580
|
+
this.autoEnd = autoEnd;
|
|
581
|
+
}
|
|
582
|
+
spanContext() { return INVALID_SPAN_CONTEXT; }
|
|
583
|
+
setAttribute(key, value) {
|
|
584
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
585
|
+
this.nativeSpan.setAttribute(key, value);
|
|
586
|
+
}
|
|
587
|
+
else if (Array.isArray(value)) {
|
|
588
|
+
this.nativeSpan.setAttribute(key, JSON.stringify(value));
|
|
589
|
+
}
|
|
590
|
+
return this;
|
|
591
|
+
}
|
|
592
|
+
setAttributes(attributes) {
|
|
593
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
594
|
+
if (value !== undefined)
|
|
595
|
+
this.setAttribute(key, value);
|
|
596
|
+
}
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
addEvent(name, attributesOrStartTime) {
|
|
600
|
+
const prefix = `event.${name}`;
|
|
601
|
+
this.nativeSpan.setAttribute(prefix, true);
|
|
602
|
+
if (attributesOrStartTime && !Array.isArray(attributesOrStartTime) && typeof attributesOrStartTime === "object") {
|
|
603
|
+
for (const [key, value] of Object.entries(attributesOrStartTime)) {
|
|
604
|
+
if (value !== undefined)
|
|
605
|
+
this.setAttribute(`${prefix}.${key}`, value);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
addLink(_link) { return this; }
|
|
611
|
+
addLinks(_links) { return this; }
|
|
612
|
+
setStatus(status) {
|
|
613
|
+
if (status.code === SpanStatusCode.ERROR) {
|
|
614
|
+
this.nativeSpan.setAttribute("error.type", "error");
|
|
615
|
+
if (status.message)
|
|
616
|
+
this.nativeSpan.setAttribute("error.message", status.message);
|
|
617
|
+
}
|
|
618
|
+
return this;
|
|
619
|
+
}
|
|
620
|
+
updateName(_name) { return this; }
|
|
621
|
+
end(_endTime) {
|
|
622
|
+
if (!this.autoEnd)
|
|
623
|
+
this.nativeSpan.end();
|
|
624
|
+
}
|
|
625
|
+
isRecording() { return this.nativeSpan.isTraced; }
|
|
626
|
+
recordException(exception, _time) {
|
|
627
|
+
const error = exception instanceof Error ? exception : new Error(String(exception));
|
|
628
|
+
this.nativeSpan.setAttribute("error.type", error.name || "Error");
|
|
629
|
+
this.nativeSpan.setAttribute("error.message", error.message);
|
|
630
|
+
if (error.stack)
|
|
631
|
+
this.nativeSpan.setAttribute("exception.stacktrace", error.stack);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function getNativeTracing(ctx) {
|
|
635
|
+
const candidate = ctx.tracing;
|
|
636
|
+
return candidate && typeof candidate.enterSpan === "function" ? candidate : undefined;
|
|
637
|
+
}
|
|
638
|
+
function setNativeRequestAttributes(span, config, req) {
|
|
639
|
+
const url = new URL(req.url);
|
|
640
|
+
span.setAttribute("service.name", config.service);
|
|
641
|
+
span.setAttribute("http.route", url.pathname);
|
|
642
|
+
if (config.version)
|
|
643
|
+
span.setAttribute("service.version", config.version);
|
|
644
|
+
if (config.build)
|
|
645
|
+
span.setAttribute("service.build", config.build);
|
|
646
|
+
const userInfo = config.getUser?.(req);
|
|
647
|
+
if (userInfo?.id)
|
|
648
|
+
span.setAttribute("enduser.id", userInfo.id);
|
|
649
|
+
if (userInfo?.session)
|
|
650
|
+
span.setAttribute("session.id", userInfo.session);
|
|
651
|
+
const requestId = userInfo?.requestId ?? req.headers.get("cf-ray") ?? undefined;
|
|
652
|
+
if (requestId)
|
|
653
|
+
span.setAttribute("http.request.id", requestId);
|
|
654
|
+
}
|
|
655
|
+
async function runNativeSpan(nativeTracing, name, attrs, fn) {
|
|
656
|
+
ensureContextManager();
|
|
657
|
+
return nativeTracing.enterSpan(name, async (nativeSpan) => {
|
|
658
|
+
const adapter = new NativeSpanAdapter(nativeSpan, true);
|
|
659
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
660
|
+
if (value !== undefined)
|
|
661
|
+
adapter.setAttribute(key, value);
|
|
662
|
+
}
|
|
663
|
+
const nativeContext = trace.setSpan(context.active().setValue(NATIVE_TRACING_CONTEXT_KEY, nativeTracing), adapter);
|
|
664
|
+
try {
|
|
665
|
+
return await context.with(nativeContext, () => fn(adapter));
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
669
|
+
adapter.recordException(err);
|
|
670
|
+
adapter.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
566
675
|
/**
|
|
567
676
|
* Build a `BasicTracerProvider` wired to a `HeystackSpanExporter` via a
|
|
568
677
|
* `SimpleSpanProcessor`. Exposed for advanced users who want to add manual
|
|
@@ -824,6 +933,20 @@ export function instrument(handler, config) {
|
|
|
824
933
|
const originalFetch = handler.fetch?.bind(handler);
|
|
825
934
|
if (originalFetch) {
|
|
826
935
|
wrapped.fetch = async (req, env, ctx) => {
|
|
936
|
+
const nativeTracing = config.nativeTracing === "direct" ? undefined : getNativeTracing(ctx);
|
|
937
|
+
if (nativeTracing) {
|
|
938
|
+
// `withSpan()` discovers the request's native tracer through OTel
|
|
939
|
+
// context. Register our WinterCG-safe manager even though native mode
|
|
940
|
+
// intentionally does not create an OTel provider/exporter.
|
|
941
|
+
return runNativeSpan(nativeTracing, "heystack.request", {}, async (adapter) => {
|
|
942
|
+
setNativeRequestAttributes(adapter, config, req);
|
|
943
|
+
const response = await originalFetch(req, env, ctx);
|
|
944
|
+
adapter.setAttribute("http.response.status_code", response.status);
|
|
945
|
+
if (response.status >= 500)
|
|
946
|
+
adapter.setAttribute("error.type", "http_error");
|
|
947
|
+
return response;
|
|
948
|
+
});
|
|
949
|
+
}
|
|
827
950
|
const s = setup(env);
|
|
828
951
|
if (!s)
|
|
829
952
|
return originalFetch(req, env, ctx);
|
|
@@ -998,6 +1121,17 @@ export function instrument(handler, config) {
|
|
|
998
1121
|
const originalQueue = handler.queue?.bind(handler);
|
|
999
1122
|
if (originalQueue) {
|
|
1000
1123
|
wrapped.queue = async (batch, env, ctx) => {
|
|
1124
|
+
const nativeTracing = config.nativeTracing === "direct" ? undefined : getNativeTracing(ctx);
|
|
1125
|
+
if (nativeTracing) {
|
|
1126
|
+
return runNativeSpan(nativeTracing, `heystack.queue ${batch.queue}`, {
|
|
1127
|
+
"service.name": config.service,
|
|
1128
|
+
"messaging.batch.message_count": batch.messages.length,
|
|
1129
|
+
"messaging.destination.name": batch.queue,
|
|
1130
|
+
"messaging.operation.type": "process",
|
|
1131
|
+
"service.version": config.version,
|
|
1132
|
+
"service.build": config.build,
|
|
1133
|
+
}, () => originalQueue(batch, env, ctx));
|
|
1134
|
+
}
|
|
1001
1135
|
const s = setup(env);
|
|
1002
1136
|
if (!s)
|
|
1003
1137
|
return originalQueue(batch, env, ctx);
|
|
@@ -1030,6 +1164,16 @@ export function instrument(handler, config) {
|
|
|
1030
1164
|
const originalScheduled = handler.scheduled?.bind(handler);
|
|
1031
1165
|
if (originalScheduled) {
|
|
1032
1166
|
wrapped.scheduled = async (controller, env, ctx) => {
|
|
1167
|
+
const nativeTracing = config.nativeTracing === "direct" ? undefined : getNativeTracing(ctx);
|
|
1168
|
+
if (nativeTracing) {
|
|
1169
|
+
return runNativeSpan(nativeTracing, `heystack.scheduled ${controller.cron}`, {
|
|
1170
|
+
"service.name": config.service,
|
|
1171
|
+
"faas.trigger": "timer",
|
|
1172
|
+
"cloudflare.scheduled.cron": controller.cron,
|
|
1173
|
+
"service.version": config.version,
|
|
1174
|
+
"service.build": config.build,
|
|
1175
|
+
}, () => originalScheduled(controller, env, ctx));
|
|
1176
|
+
}
|
|
1033
1177
|
const s = setup(env);
|
|
1034
1178
|
if (!s)
|
|
1035
1179
|
return originalScheduled(controller, env, ctx);
|
|
@@ -1063,6 +1207,23 @@ export function instrument(handler, config) {
|
|
|
1063
1207
|
export async function withSpan(name, attrsOrFn, maybeFn) {
|
|
1064
1208
|
const attrs = typeof attrsOrFn === "function" ? undefined : attrsOrFn;
|
|
1065
1209
|
const fn = (typeof attrsOrFn === "function" ? attrsOrFn : maybeFn);
|
|
1210
|
+
const nativeTracing = nativeTracingFromContext();
|
|
1211
|
+
if (nativeTracing) {
|
|
1212
|
+
return nativeTracing.enterSpan(name, async (nativeSpan) => {
|
|
1213
|
+
const adapter = new NativeSpanAdapter(nativeSpan, true);
|
|
1214
|
+
if (attrs)
|
|
1215
|
+
adapter.setAttributes(attrs);
|
|
1216
|
+
const nativeContext = trace.setSpan(context.active().setValue(NATIVE_TRACING_CONTEXT_KEY, nativeTracing), adapter);
|
|
1217
|
+
try {
|
|
1218
|
+
return await context.with(nativeContext, () => fn(adapter));
|
|
1219
|
+
}
|
|
1220
|
+
catch (err) {
|
|
1221
|
+
adapter.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
1222
|
+
adapter.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
|
|
1223
|
+
throw err;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1066
1227
|
const tracer = trace.getTracer("heystack");
|
|
1067
1228
|
const span = tracer.startSpan(name, attrs ? { attributes: attrs } : undefined);
|
|
1068
1229
|
const ctx = trace.setSpan(context.active(), span);
|
|
@@ -1078,6 +1239,14 @@ export async function withSpan(name, attrsOrFn, maybeFn) {
|
|
|
1078
1239
|
span.end();
|
|
1079
1240
|
}
|
|
1080
1241
|
}
|
|
1242
|
+
export async function withCloudflareSpan(ctx, name, attrsOrFn, maybeFn) {
|
|
1243
|
+
const attrs = typeof attrsOrFn === "function" ? {} : attrsOrFn;
|
|
1244
|
+
const fn = (typeof attrsOrFn === "function" ? attrsOrFn : maybeFn);
|
|
1245
|
+
const tracing = "enterSpan" in ctx ? ctx : getNativeTracing(ctx);
|
|
1246
|
+
if (tracing)
|
|
1247
|
+
return runNativeSpan(tracing, name, attrs, fn);
|
|
1248
|
+
return withSpan(name, attrs, fn);
|
|
1249
|
+
}
|
|
1081
1250
|
/**
|
|
1082
1251
|
* Add a named event (with optional attributes) to the currently-active span.
|
|
1083
1252
|
* No-op when no span is active.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heystack/otel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Runtime-aware OpenTelemetry tracing that exports to Heystack (Node, Next.js, Workers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./dist/workers.d.ts",
|
|
24
24
|
"import": "./dist/workers.js"
|
|
25
25
|
},
|
|
26
|
+
"./workers/agents": {
|
|
27
|
+
"types": "./dist/workers-agents.d.ts",
|
|
28
|
+
"import": "./dist/workers-agents.js"
|
|
29
|
+
},
|
|
26
30
|
"./web": {
|
|
27
31
|
"types": "./dist/web.d.ts",
|
|
28
32
|
"import": "./dist/web.js"
|