@heystack/otel 0.19.0 → 0.19.1
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 +2 -1
- package/dist/workers-bindings.js +37 -2
- package/dist/workers.js +56 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -480,7 +480,7 @@ The automatic `gen_ai.*` enrichment hooks `fetch`. Anything that doesn't go thro
|
|
|
480
480
|
import { instrument, withSpan, genAiRequestAttrs, genAiResponseAttrs } from "@heystack/otel/workers";
|
|
481
481
|
|
|
482
482
|
async function chat(env: Env, prompt: string) {
|
|
483
|
-
const model = "@cf/meta/llama-3.1-8b-instruct";
|
|
483
|
+
const model = "@cf/meta/llama-3.1-8b-instruct-fast";
|
|
484
484
|
return withSpan(
|
|
485
485
|
"chat cloudflare",
|
|
486
486
|
{ ...genAiRequestAttrs("cloudflare", { model, max_tokens: 256 }), "gen_ai.operation.name": "chat" },
|
|
@@ -732,6 +732,7 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
732
732
|
|
|
733
733
|
## Migration / versioning
|
|
734
734
|
|
|
735
|
+
- **`0.19.1`** — **Workers fixes found by the live Cloudflare run.** (1) A Durable Object class that extends `DurableObject` from `cloudflare:workers` and is wrapped with `wrapDurableObjectClass` failed to construct on Cloudflare (`constructor parameter 1 is not of type 'DurableObjectState'`): the base class now receives the genuine state, and the traced view replaces it on the instance, so `this.ctx.storage` stays traced. (2) With `instrumentBindings`, a service binding to a `WorkerEntrypoint` was mistaken for a D1 binding (its RPC stub answers every property name), so every RPC call through it failed (`The RPC receiver does not implement the method "bind"`); RPC service bindings are now detected first and their stubs pass through untouched, including nested property chains. (3) `flushHeystack()` (and the drain handed to `waitUntil`) no longer rejects when an export fails, so a down ingest or a rejected key never surfaces as an error in your Worker. No API changes; upgrade and redeploy.
|
|
735
736
|
- **`0.19.0`** — **Cloudflare native spans, coverage gate and direct-mode parity, plus privacy defaults.** **Privacy defaults:** (1) Recorded URLs are redacted on every entry: userinfo stripped and sensitive query/fragment values (`key`, `apikey`, `token`, `code`, `access_token`, `X-Amz-Signature`, …) replaced with `REDACTED`; extend with `redactQueryParams: [...]` or opt out with `false`. (2) `client.address` is **no longer recorded by default** on `/workers` and is stripped from `/node` spans (with `http.client_ip` and SERVER-span peer IPs) — set `captureClientIp: true` to keep it. (3) `/workers` geo defaults to country only — `captureGeo: "full"` restores `geo.region`/`geo.city`/`geo.asn`. (4) `/web` strict replay masking now masks **all** page text (previously only inputs); use `data-hs-unmask` to reveal trusted elements, and a missing masking mode is treated as strict. (5) New `/workers` `trustInboundSampling` (default `true`). See [Privacy](#privacy-url-redaction--client-ip). **Cloudflare:** (1) **Binding attributes.** Every direct-mode binding span carries `cloudflare.binding.type` (`d1`, `kv`, `r2`, `durable_object`, `queue`, `ai`, `vectorize`, `workflow`, `ratelimit`, `analytics_engine`) and `cloudflare.binding.name` (the env key). **Rename:** the rate-limit type value `rate_limit` is now `ratelimit` (ingest accepts both). Queue spans add `cloudflare.queue.name`. (2) **D1** spans add `db.system.name: "cloudflare-d1"` and `db.query.text`; the older `db.system` / `db.statement` stay for one more minor version and will then be removed. (3) **Hashed keys (behaviour change).** KV keys and R2 object keys are no longer recorded raw: `kv.key` / `r2.key` / `cloudflare.do.storage.key` are replaced by `cloudflare.kv.query.keys`, `cloudflare.r2.request.key` and a hashed `cloudflare.do.storage.key` (the prefix through the first `:` or `/` is kept, the rest becomes `h:<16 hex>`; multi-key calls are comma-joined). If Web Crypto is unavailable the attribute is omitted, never the raw key. Hashing runs concurrently and never delays or changes the call; multi-key calls hash at most the first 10 keys and report the full count in `cloudflare.kv.query.key_count` / `cloudflare.r2.request.key_count`. **PII note:** the key prefix through the first `:` or `/` is kept verbatim (e.g. `avatars/h:…`), so never put personal data in key prefixes. Service-binding spans also carry `cloudflare.binding.type: "service"`. `wrapWorkerEntrypointClass` also traces the entrypoint's own `this.env` bindings when `instrumentBindings` is set. (4) **Durable Objects.** `ctx.storage.sql.exec()` emits `durable_object_storage_exec` (`db.query.text`, `cloudflare.durable_object.response.rows_read|rows_written` once the app finishes the cursor, `cloudflare.durable_object.id`); the cursor is never iterated by the SDK. `getAlarm` / `setAlarm` / `deleteAlarm` emit `durable_object_alarms_*`. WebSocket hibernation handlers are now named `DO <Class> webSocketMessage|webSocketClose|webSocketError` (previously `DO rpc …`) with `cloudflare.handler_type`. (5) **New `wrapWorkerEntrypointClass(cls, config)`** for `WorkerEntrypoint` classes: traces `fetch` like `instrument()` and each public RPC method as a SERVER span `rpc <method>` (`jsrpc.method`, `rpc.method`). RPC spans are direct-mode only; under native tracing Cloudflare already emits `jsrpc` spans, so RPC passes straight through. (6) The direct-mode resource now carries `cloud.provider: "cloudflare"` and `cloud.platform: "cloudflare.workers"`. **Migration notes (0.19):** (a) In native mode `http.route` is no longer the raw path: pass `routes` to get templated routes. (b) `kv.key` / `r2.key` are replaced by the hashed native attribute names above. (c) `db.system` / `db.statement` are deprecated in favour of `db.system.name` / `db.query.text`. (d) Binding type `rate_limit` is now `ratelimit`. (e) `CloudflareNativeTracing` / `CloudflareNativeSpan` remain exported as deprecated aliases of `CloudflareTracing` / `CloudflareSpan`. (f) Native enrichment is on for the kinds Cloudflare does not span itself (Workers AI, Vectorize, Workflows, Analytics Engine; Hyperdrive via `wrapSqlClient`); disable it per kind with `nativeEnrichment`, and an explicit `instrumentBindings` overrides the gate. Apps that already wrap AI calls manually may see a duplicate span until they opt out. (g) New `ai.gatewayExport`. (h) New `/workers` exports: `wrapWorkerEntrypointClass`, `wrapSqlClient`, `NATIVELY_COVERED`, `matchRoute`, and the types `CloudflareTracing`, `CloudflareSpan`, `BindingKind`.
|
|
736
737
|
- **`0.16.0`** — **complete direct Workers lifecycle and binding coverage.** Durable Object namespace stubs now trace `fetch` and RPC calls with W3C trace propagation; Workflow bindings trace creation, batch creation, lookup, instance lifecycle methods, and deletion operations. Direct instrumentation also covers scheduled, queue, email, Tail Worker, and related Worker entry points, while preserving synchronous values, native promises, custom thenables, upgrade responses, host-object identity, and callback-scoped native span ownership. Service-binding propagation now respects caller-supplied request headers. No breaking changes.
|
|
737
738
|
- **`0.15.0`** — **direct Workers D1 session and batch coverage.** With `nativeTracing: "direct"` and `instrumentBindings` enabled, prepared D1 statements, database/session `batch`, database `exec`, and statements created through `withSession` now receive child spans. Native Cloudflare OTel remains the recommended Workers path because it preserves Cloudflare's platform trace/log tree; use direct export only where `ctx.tracing` is unavailable. No breaking changes.
|
package/dist/workers-bindings.js
CHANGED
|
@@ -63,6 +63,18 @@ function isAnalyticsEngineLike(b) {
|
|
|
63
63
|
function isServiceLike(b) {
|
|
64
64
|
return typeof b?.fetch === "function";
|
|
65
65
|
}
|
|
66
|
+
/** A service binding to a WorkerEntrypoint answers EVERY property name with a callable
|
|
67
|
+
* JS RPC stub, so it would pass every duck-typed check above (`prepare` looks like D1).
|
|
68
|
+
* No real binding has a function under this name: one that does is an RPC stub. */
|
|
69
|
+
const RPC_PROBE = "__heystackRpcProbe__";
|
|
70
|
+
function isRpcServiceLike(b) {
|
|
71
|
+
try {
|
|
72
|
+
return typeof b?.fetch === "function" && typeof b?.[RPC_PROBE] === "function";
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
66
78
|
/**
|
|
67
79
|
* Adapt a callback-scoped `withSpan` to the immediate-return `startSpan` shape
|
|
68
80
|
* the legacy wrappers use. The callback stays open (pending promise) until the
|
|
@@ -668,7 +680,7 @@ export function injectTraceparentArgs(args, traceparent) {
|
|
|
668
680
|
return [input, init];
|
|
669
681
|
}
|
|
670
682
|
function wrapService(binding, opts, name) {
|
|
671
|
-
|
|
683
|
+
const handlers = {
|
|
672
684
|
fetch: async (...args) => {
|
|
673
685
|
if (isTracingSuppressed(context.active())) {
|
|
674
686
|
return binding.fetch(...args);
|
|
@@ -681,6 +693,24 @@ function wrapService(binding, opts, name) {
|
|
|
681
693
|
: args;
|
|
682
694
|
return runWithSpan(span, () => binding.fetch(...injectedArgs));
|
|
683
695
|
},
|
|
696
|
+
};
|
|
697
|
+
if (!isRpcServiceLike(binding))
|
|
698
|
+
return makeProxy(binding, handlers);
|
|
699
|
+
// JS RPC binding: every other property is a callable stub that must reach the caller
|
|
700
|
+
// untouched. Reading `.bind` on it would itself be an RPC call (to a method named
|
|
701
|
+
// "bind"), and a bound copy would lose nested property chains (`env.SVC.users.get()`).
|
|
702
|
+
return new Proxy(binding, {
|
|
703
|
+
get(target, prop) {
|
|
704
|
+
if (prop === rawTarget)
|
|
705
|
+
return target;
|
|
706
|
+
if (prop === "fetch")
|
|
707
|
+
return handlers.fetch;
|
|
708
|
+
const value = Reflect.get(target, prop, target);
|
|
709
|
+
// connect() is a host method on the Fetcher and needs its receiver.
|
|
710
|
+
if (prop === "connect" && typeof value === "function")
|
|
711
|
+
return Function.prototype.bind.call(value, target);
|
|
712
|
+
return value;
|
|
713
|
+
},
|
|
684
714
|
});
|
|
685
715
|
}
|
|
686
716
|
// ---------------------------------------------------------------------------
|
|
@@ -938,7 +968,12 @@ export function instrumentEnv(env, rawOpts) {
|
|
|
938
968
|
if (select !== true && !select.includes(key))
|
|
939
969
|
continue;
|
|
940
970
|
const binding = env[key];
|
|
941
|
-
if (
|
|
971
|
+
if (isRpcServiceLike(binding)) {
|
|
972
|
+
// First: an RPC stub matches every duck-typed check below.
|
|
973
|
+
if (allow("service"))
|
|
974
|
+
result[key] = wrapService(binding, stamped("service", key), key);
|
|
975
|
+
}
|
|
976
|
+
else if (isD1Like(binding)) {
|
|
942
977
|
if (allow("d1"))
|
|
943
978
|
result[key] = wrapD1(binding, stamped("d1", key));
|
|
944
979
|
}
|
package/dist/workers.js
CHANGED
|
@@ -1210,12 +1210,25 @@ export async function flushHeystack() {
|
|
|
1210
1210
|
new Promise((resolve) => { timer = setTimeout(resolve, DRAIN_TIMEOUT_MS); }),
|
|
1211
1211
|
]).finally(() => { if (timer)
|
|
1212
1212
|
clearTimeout(timer); });
|
|
1213
|
-
|
|
1214
|
-
|
|
1213
|
+
// Telemetry must never fail the app: OpenTelemetry's provider.forceFlush rejects (with an
|
|
1214
|
+
// array) when a processor fails or times out, and that would surface in an awaited
|
|
1215
|
+
// flushHeystack() or as an uncaught waitUntil error on every request while ingest is down.
|
|
1216
|
+
try {
|
|
1217
|
+
if (typeof _provider.forceFlush === "function") {
|
|
1218
|
+
await _provider.forceFlush();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
// Dropped batch: the exporter already reported it; still drain what is in flight.
|
|
1215
1223
|
}
|
|
1216
1224
|
// The processor's forceFlush does not await our fire-and-forget fetch, so
|
|
1217
1225
|
// drain the exporter's in-flight POSTs explicitly.
|
|
1218
|
-
|
|
1226
|
+
try {
|
|
1227
|
+
await _provider.heystackExporter.forceFlush();
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
// Same rule: an export failure is never the app's error.
|
|
1231
|
+
}
|
|
1219
1232
|
}
|
|
1220
1233
|
/**
|
|
1221
1234
|
* Wrap a Worker `ExecutionContext` so EVERY `ctx.waitUntil(promise)` ALSO
|
|
@@ -2151,6 +2164,21 @@ const doIdAttrs = (state) => {
|
|
|
2151
2164
|
const id = durableObjectIdOf(state);
|
|
2152
2165
|
return id ? { "cloudflare.durable_object.id": id } : {};
|
|
2153
2166
|
};
|
|
2167
|
+
/** True when a class (or an ancestor) is named `name`, e.g. cloudflare:workers' `DurableObject`. */
|
|
2168
|
+
function extendsNamedBase(cls, name) {
|
|
2169
|
+
try {
|
|
2170
|
+
let c = Object.getPrototypeOf(cls);
|
|
2171
|
+
for (let depth = 0; depth < 8 && typeof c === "function"; depth++) {
|
|
2172
|
+
if (c.name === name)
|
|
2173
|
+
return true;
|
|
2174
|
+
c = Object.getPrototypeOf(c);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
catch {
|
|
2178
|
+
// Exotic prototype chains are treated as plain classes.
|
|
2179
|
+
}
|
|
2180
|
+
return false;
|
|
2181
|
+
}
|
|
2154
2182
|
/** Proxy a DurableObjectState so `state.storage` resolves to the traced proxy. */
|
|
2155
2183
|
function wrapDurableObjectState(state, startSpan) {
|
|
2156
2184
|
if (!state || typeof state !== "object")
|
|
@@ -2230,12 +2258,35 @@ export function wrapDurableObjectClass(cls, opts = {}) {
|
|
|
2230
2258
|
catch {
|
|
2231
2259
|
// Best-effort enumeration — a hostile getter must never break decoration.
|
|
2232
2260
|
}
|
|
2261
|
+
// workerd's `DurableObject` base (cloudflare:workers) brand-checks its first argument, so a
|
|
2262
|
+
// subclass of it must receive the genuine state. Classes without that base keep getting the
|
|
2263
|
+
// traced view directly, which also traces storage calls made inside their constructor.
|
|
2264
|
+
const nativeBase = extendsNamedBase(Base, "DurableObject");
|
|
2233
2265
|
class Wrapped extends Base {
|
|
2234
2266
|
/** Raw (unwrapped) state captured for flush scheduling. */
|
|
2235
2267
|
__heystackState;
|
|
2236
2268
|
constructor(...args) {
|
|
2237
|
-
|
|
2238
|
-
|
|
2269
|
+
const raw = args[0];
|
|
2270
|
+
const traced = wrapDurableObjectState(raw, startSpan);
|
|
2271
|
+
super(...(nativeBase ? args : [traced, ...args.slice(1)]));
|
|
2272
|
+
this.__heystackState = raw;
|
|
2273
|
+
// Swap the traced view in wherever the instance holds the state (`this.ctx` from the
|
|
2274
|
+
// base, or a field the user assigned), so `this.ctx.storage` calls are traced.
|
|
2275
|
+
if (nativeBase && traced !== raw) {
|
|
2276
|
+
const self = this;
|
|
2277
|
+
for (const key of new Set(["ctx", "state", ...Object.keys(self)])) {
|
|
2278
|
+
if (key === "__heystackState")
|
|
2279
|
+
continue;
|
|
2280
|
+
try {
|
|
2281
|
+
if (self[key] === raw) {
|
|
2282
|
+
Object.defineProperty(self, key, { value: traced, writable: true, configurable: true, enumerable: true });
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
catch {
|
|
2286
|
+
// A non-configurable or frozen slot keeps the raw state: calls stay untraced, never broken.
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2239
2290
|
}
|
|
2240
2291
|
}
|
|
2241
2292
|
if (typeof origFetch === "function") {
|