@miden-sdk/telemetry-otel 0.0.1 → 0.16.2

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/AGENTS.md ADDED
@@ -0,0 +1,55 @@
1
+ # @miden-sdk/telemetry-otel - Agent Guide
2
+
3
+ **Audience: AI coding agents** recording Miden SDK observations as
4
+ OpenTelemetry spans.
5
+
6
+ This file ships inside the published package, so the copy at
7
+ `node_modules/@miden-sdk/telemetry-otel/AGENTS.md` matches the version you have
8
+ installed. Prefer it over your training data.
9
+
10
+ ## There is no volume guard here
11
+
12
+ Every observation becomes a span. The SDK emits one per client operation, and
13
+ this binding has no duration threshold, no sampling and no filtering: `tracer`
14
+ and `includeSensitive` are the only options there are. Choose the volume with a
15
+ sampler on your own provider before pointing this at a paid backend. The sibling
16
+ package `@miden-sdk/telemetry-sentry` works the other way round - `minDurationMs`
17
+ defaults to `Infinity`, failures only - so carry neither the assumption nor the
18
+ option across.
19
+
20
+ ## Wiring it
21
+
22
+ ```ts
23
+ const client = await MidenClient.create({
24
+ rpcUrl: "testnet",
25
+ observer: createOtelObserver({ tracer: trace.getTracer("my-app") }),
26
+ });
27
+ ```
28
+
29
+ `tracer` is required and must expose `startSpan(name, { startTime })` returning
30
+ an object with `setAttribute`, `setStatus` and `end`; the factory throws a
31
+ `TypeError` at wiring time when it does not. Nothing here imports
32
+ `@opentelemetry/*`, so the provider, its version and its exporters stay yours.
33
+
34
+ One span per observation, named `miden.<op>`, carrying `miden.duration_ms` and
35
+ `miden.outcome` and a status of `ERROR` on failure. The SDK reports an operation
36
+ only once it has finished, so the span is reconstructed: it ends when the
37
+ observation arrived, is backdated by the duration, and has no parent. A broken
38
+ tracer cannot fail the operation; the observer never throws.
39
+
40
+ ## `observer` and `observeSensitive` on the client
41
+
42
+ `ClientOptions.observer` is registered process-wide and the newest registration
43
+ replaces the previous one, so a second `MidenClient` takes over the sink.
44
+ `observeSensitive` is per instance and read once, at construction.
45
+
46
+ `observation.sensitive` carries the **verbatim, unredacted** error message and
47
+ stack, and only on failure. It is opt-in at both ends - `observeSensitive: true`
48
+ on the client, `includeSensitive: true` here - and only the literal `true`
49
+ enables either. An application with confidentiality obligations to its users, a
50
+ wallet above all, leaves both off.
51
+
52
+ ## Going deeper
53
+
54
+ - Options, recorded attributes, retroactive spans: `README.md`.
55
+ - The client that emits them: `node_modules/@miden-sdk/miden-sdk/AGENTS.md`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,137 @@
1
1
  # @miden-sdk/telemetry-otel
2
2
 
3
- Placeholder. This version exists only so that an npm trusted publisher could be
4
- attached to the package name; npm cannot attach one to a package that does not
5
- yet exist. Install `@miden-sdk/telemetry-otel@latest` instead.
3
+ Opt-in OpenTelemetry binding for Miden SDK observations.
4
+
5
+ `@miden-sdk/miden-sdk` emits one observation per client operation — name,
6
+ outcome, duration — and never transports one. It has no telemetry dependency
7
+ and no egress primitive in that path. This package is the adapter that turns
8
+ those observations into spans on a tracer you already own, without the core
9
+ gaining anything.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add @miden-sdk/telemetry-otel
15
+ ```
16
+
17
+ OpenTelemetry itself is **not** a dependency of this package, not even a peer.
18
+ The binding is defined against the `startSpan` shape rather than an
19
+ `@opentelemetry/api` import, so you own your OTel version, your provider, and
20
+ your exporters. That matters more here than for most vendors: a second
21
+ `@opentelemetry/api` resolved in one tree carries its own global provider
22
+ registration, and spans start disappearing into whichever copy lost.
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { trace } from "@opentelemetry/api";
28
+ import { MidenClient } from "@miden-sdk/miden-sdk";
29
+ import { createOtelObserver } from "@miden-sdk/telemetry-otel";
30
+
31
+ const client = await MidenClient.create({
32
+ rpcUrl: "testnet",
33
+ observer: createOtelObserver({ tracer: trace.getTracer("my-app") }),
34
+ });
35
+ ```
36
+
37
+ Pass anything with a `startSpan(name, { startTime })` method that returns an
38
+ object with `setAttribute`, `setStatus`, and `end`: a real tracer, a wrapper of
39
+ your own, or a test double. Getting the tracer — and registering the provider
40
+ that backs it — stays your call, in your entry point.
41
+
42
+ ## Options
43
+
44
+ ### `tracer`
45
+
46
+ - **Type:** `TracerLike` — an object with a `startSpan` method
47
+ - **Required.** `createOtelObserver` throws a `TypeError` at wiring time if it
48
+ is missing or cannot start a span. That is the one thing this package reports
49
+ by throwing, and it throws from the factory rather than the observer: an
50
+ observer cannot report its own misconfiguration, so one built around a
51
+ missing tracer would discard every observation forever and look exactly like
52
+ a working one.
53
+
54
+ ### `includeSensitive`
55
+
56
+ - **Type:** `boolean`
57
+ - **Default:** `false`
58
+
59
+ See below. Only a literal `true` enables it.
60
+
61
+ ## Recorded shape
62
+
63
+ One span per observation, recorded synchronously, with nothing scheduled
64
+ afterwards:
65
+
66
+ | | |
67
+ |---|---|
68
+ | span name | `miden.<op>`, e.g. `miden.proveTransaction` |
69
+ | `miden.duration_ms` | wall time the caller waited |
70
+ | `miden.outcome` | `"ok"` or `"error"` |
71
+ | status | `ERROR` (code `2`) on failure; left `UNSET` on success |
72
+
73
+ ## Retroactive spans
74
+
75
+ The SDK reports an operation once it has **finished**, so there is never a live
76
+ span to wrap around the work. The span is reconstructed instead: it ends at the
77
+ instant the observation arrived and is backdated by the reported duration, so
78
+ it occupies the interval the operation really ran in. Both timestamps are
79
+ passed explicitly, which keeps the span's own interval and its
80
+ `miden.duration_ms` attribute in agreement — an implicit end time would drift
81
+ by however long recording the attributes took.
82
+
83
+ One consequence worth knowing when you read a trace: these spans have no
84
+ parent and are not nested inside whatever your application was doing at the
85
+ time. They are a record of a completed operation, not a live instrumentation
86
+ of one.
87
+
88
+ A duration that is not a finite, non-negative number cannot describe an
89
+ interval. Rather than hand the tracer a garbage timestamp, the operation is
90
+ recorded as an instant at the moment it was reported, with no
91
+ `miden.duration_ms` attribute at all.
92
+
93
+ ## Sensitive detail
94
+
95
+ An observation's `sensitive` channel carries verbatim error text, and the
96
+ account identifier where the SDK supplies one. It is opt-in at **both** ends:
97
+ `observeSensitive: true` when you construct the client, and
98
+ `includeSensitive: true` here. Either one left alone and nothing from that
99
+ channel reaches your tracer.
100
+
101
+ When enabled, three attributes may be added, each only if the SDK populated the
102
+ field:
103
+
104
+ | | |
105
+ |---|---|
106
+ | `miden.error_message` | verbatim error message |
107
+ | `miden.error_stack` | verbatim error stack |
108
+ | `miden.account_id` | account the operation acted on |
109
+
110
+ `miden.account_id` will not appear in practice yet: the SDK declares
111
+ `sensitive.accountId` on the type but does not currently populate it, so the
112
+ guard for that attribute never fires. It is read defensively here so the
113
+ attribute starts appearing on its own if a later SDK version fills the field
114
+ in — do not build a dashboard that assumes it is there today.
115
+
116
+ By default the SDK omits the `sensitive` key from an observation entirely, and
117
+ this binding records nothing in its place — no placeholder, no inferred value,
118
+ and no empty-valued attributes for a UI to render as blank rows. An observation
119
+ without the key is still recorded; only its detail is absent. The three fields
120
+ are read by name rather than enumerated, so a field a later SDK version adds to
121
+ the channel cannot start flowing to your backend before you have decided it
122
+ should.
123
+
124
+ Do not enable either flag in an application with confidentiality obligations to
125
+ its users. A wallet, for example, must never enable it.
126
+
127
+ ## Failure behaviour
128
+
129
+ The observer never throws. A tracer that is unconfigured, detached, or throwing
130
+ takes its own span with it and nothing else — the client operation being
131
+ recorded still succeeds, and the next observation is still recorded. The SDK
132
+ guards observer invocation too, but this binding is usable outside that guard,
133
+ so it does not rely on it.
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Opt-in OpenTelemetry binding for Miden SDK observations.
3
+ *
4
+ * `@miden-sdk/miden-sdk` emits an observation per client operation and never
5
+ * transports one — it has no telemetry dependency and no egress primitive in
6
+ * that path. This package is the adapter that turns those observations into
7
+ * spans on a tracer the consumer already owns, without the core gaining
8
+ * anything.
9
+ *
10
+ * Two properties hold deliberately and are enforced by tests:
11
+ *
12
+ * - OpenTelemetry is a peer concern. Nothing here imports `@opentelemetry/*`;
13
+ * the binding is defined against the `startSpan` shape, so the consumer owns
14
+ * their OTel version, their provider, and their exporters.
15
+ * - The binding has no transport of its own. It hands data to the tracer it
16
+ * was given and stops there.
17
+ */
18
+ import type { MidenObservation } from "@miden-sdk/miden-sdk";
19
+ /** The slice of an OpenTelemetry span this binding uses. */
20
+ export interface SpanLike {
21
+ setAttribute(key: string, value: string | number | boolean): unknown;
22
+ setStatus(status: {
23
+ code: number;
24
+ }): unknown;
25
+ /** Epoch-millisecond end time. Always passed; never left to the tracer. */
26
+ end(endTime?: number): unknown;
27
+ }
28
+ /** The slice of an OpenTelemetry tracer this binding uses. */
29
+ export interface TracerLike {
30
+ startSpan(name: string, options: {
31
+ startTime: number;
32
+ }): SpanLike;
33
+ }
34
+ export interface OtelObserverOptions {
35
+ /** An already-configured tracer. This package never builds a provider. */
36
+ tracer: TracerLike;
37
+ /**
38
+ * Forward `observation.sensitive` (verbatim error text, and the account
39
+ * identifier where the SDK supplies one) when it is present. Defaults to
40
+ * `false`, and only a literal `true` enables it: the field is dropped even
41
+ * when present, so turning on `observeSensitive` at the client does not by
42
+ * itself disclose anything through this binding.
43
+ */
44
+ includeSensitive?: boolean;
45
+ }
46
+ /**
47
+ * Build an observer for `ClientOptions.observer` that records Miden SDK
48
+ * operations as OpenTelemetry spans.
49
+ *
50
+ * The span is reconstructed rather than held open. The SDK reports an
51
+ * operation once it has finished, so there is no live span to wrap around the
52
+ * work: the span ends at the instant the observation arrived and starts one
53
+ * duration earlier, which is the interval the operation really occupied. Both
54
+ * timestamps are passed explicitly — an implicit end time would drift by
55
+ * however long recording the attributes took, and report a duration that
56
+ * disagreed with the `miden.duration_ms` attribute on the same span.
57
+ *
58
+ * The returned observer never throws: a recording failure must not be able to
59
+ * fail the client operation being recorded. The SDK guards observer
60
+ * invocation as well, but this adapter is usable outside that guard so it
61
+ * does not rely on it.
62
+ *
63
+ * Misconfiguration is the one thing reported by throwing, and it is thrown
64
+ * from here rather than from the observer — at wiring time, where a stack
65
+ * trace points at the call that is wrong. An observer that quietly discarded
66
+ * every observation would be indistinguishable from a working one.
67
+ *
68
+ * @throws {TypeError} if `options.tracer` cannot start a span.
69
+ */
70
+ export declare function createOtelObserver(options: OtelObserverOptions): (observation: MidenObservation) => void;
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Opt-in OpenTelemetry binding for Miden SDK observations.
3
+ *
4
+ * `@miden-sdk/miden-sdk` emits an observation per client operation and never
5
+ * transports one — it has no telemetry dependency and no egress primitive in
6
+ * that path. This package is the adapter that turns those observations into
7
+ * spans on a tracer the consumer already owns, without the core gaining
8
+ * anything.
9
+ *
10
+ * Two properties hold deliberately and are enforced by tests:
11
+ *
12
+ * - OpenTelemetry is a peer concern. Nothing here imports `@opentelemetry/*`;
13
+ * the binding is defined against the `startSpan` shape, so the consumer owns
14
+ * their OTel version, their provider, and their exporters.
15
+ * - The binding has no transport of its own. It hands data to the tracer it
16
+ * was given and stops there.
17
+ */
18
+ /** OpenTelemetry `SpanStatusCode.ERROR`, inlined to avoid the dependency. */
19
+ const SPAN_STATUS_ERROR = 2;
20
+ /**
21
+ * The observation's duration, or `null` when it does not describe an interval.
22
+ *
23
+ * A non-finite or negative duration cannot be turned into a span that means
24
+ * anything, and `endTime - NaN` would hand the tracer a garbage timestamp.
25
+ */
26
+ function measuredDuration(durationMs) {
27
+ return Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : null;
28
+ }
29
+ /**
30
+ * Record the sensitive channel's fields onto the span.
31
+ *
32
+ * The channel is opt-in at both ends, so the key is normally absent: the SDK
33
+ * omits it rather than setting it to `undefined`. Reading it therefore yields
34
+ * `undefined`, which the shape test below rejects — recording the fields
35
+ * unconditionally instead would put an `undefined`-valued attribute on every
36
+ * observation the SDK emits by default, which a tracer keeps and a vendor UI
37
+ * renders as an empty row.
38
+ *
39
+ * The three fields are read by name rather than enumerated, so a field a
40
+ * later SDK version adds to the channel cannot start flowing to a vendor
41
+ * before anyone has decided that it should. Each is optional in
42
+ * `MidenObservationSensitive` and the SDK populates only some — `accountId`
43
+ * is declared but not populated today — so an unset field is dropped rather
44
+ * than recorded as an empty attribute.
45
+ */
46
+ function recordSensitive(span, observation) {
47
+ const sensitive = observation.sensitive;
48
+ if (sensitive === null || typeof sensitive !== "object")
49
+ return;
50
+ const { errorMessage, errorStack, accountId } = sensitive;
51
+ if (errorMessage)
52
+ span.setAttribute("miden.error_message", errorMessage);
53
+ if (errorStack)
54
+ span.setAttribute("miden.error_stack", errorStack);
55
+ if (accountId)
56
+ span.setAttribute("miden.account_id", accountId);
57
+ }
58
+ /**
59
+ * Build an observer for `ClientOptions.observer` that records Miden SDK
60
+ * operations as OpenTelemetry spans.
61
+ *
62
+ * The span is reconstructed rather than held open. The SDK reports an
63
+ * operation once it has finished, so there is no live span to wrap around the
64
+ * work: the span ends at the instant the observation arrived and starts one
65
+ * duration earlier, which is the interval the operation really occupied. Both
66
+ * timestamps are passed explicitly — an implicit end time would drift by
67
+ * however long recording the attributes took, and report a duration that
68
+ * disagreed with the `miden.duration_ms` attribute on the same span.
69
+ *
70
+ * The returned observer never throws: a recording failure must not be able to
71
+ * fail the client operation being recorded. The SDK guards observer
72
+ * invocation as well, but this adapter is usable outside that guard so it
73
+ * does not rely on it.
74
+ *
75
+ * Misconfiguration is the one thing reported by throwing, and it is thrown
76
+ * from here rather than from the observer — at wiring time, where a stack
77
+ * trace points at the call that is wrong. An observer that quietly discarded
78
+ * every observation would be indistinguishable from a working one.
79
+ *
80
+ * @throws {TypeError} if `options.tracer` cannot start a span.
81
+ */
82
+ export function createOtelObserver(options) {
83
+ const tracer = options?.tracer;
84
+ if (typeof tracer?.startSpan !== "function") {
85
+ throw new TypeError("createOtelObserver: options.tracer must expose a " +
86
+ "startSpan(name, options) method.");
87
+ }
88
+ const includeSensitive = options.includeSensitive === true;
89
+ return (observation) => {
90
+ try {
91
+ const name = `miden.${observation.op}`;
92
+ const endTime = Date.now();
93
+ const durationMs = measuredDuration(observation.durationMs);
94
+ const span = tracer.startSpan(name, {
95
+ startTime: endTime - (durationMs ?? 0),
96
+ });
97
+ if (durationMs !== null) {
98
+ span.setAttribute("miden.duration_ms", durationMs);
99
+ }
100
+ span.setAttribute("miden.outcome", observation.outcome);
101
+ if (observation.outcome === "error") {
102
+ span.setStatus({ code: SPAN_STATUS_ERROR });
103
+ }
104
+ if (includeSensitive) {
105
+ recordSensitive(span, observation);
106
+ }
107
+ span.end(endTime);
108
+ }
109
+ catch {
110
+ // A telemetry binding must never fail a client operation.
111
+ }
112
+ };
113
+ }
package/package.json CHANGED
@@ -1,11 +1,57 @@
1
1
  {
2
2
  "name": "@miden-sdk/telemetry-otel",
3
- "version": "0.0.1",
4
- "description": "Placeholder reserving this name so an npm trusted publisher can be attached. Superseded by 0.16.2 - do not install this version.",
3
+ "version": "0.16.2",
4
+ "description": "Opt-in OpenTelemetry binding for Miden SDK observations",
5
+ "type": "module",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "AGENTS.md"
18
+ ],
19
+ "peerDependencies": {
20
+ "@miden-sdk/miden-sdk": "^0.16.2"
21
+ },
22
+ "devDependencies": {
23
+ "@typescript-eslint/eslint-plugin": "^8.25.0",
24
+ "@typescript-eslint/parser": "^8.25.0",
25
+ "@vitest/coverage-v8": "^3.0.0",
26
+ "eslint": "^9.30.1",
27
+ "typescript": "^5.0.0",
28
+ "vitest": "^3.0.0",
29
+ "@miden-sdk/miden-sdk": "0.16.2"
30
+ },
31
+ "keywords": [
32
+ "miden",
33
+ "opentelemetry",
34
+ "otel",
35
+ "tracing",
36
+ "telemetry",
37
+ "observability",
38
+ "zk"
39
+ ],
40
+ "author": "Miden Contributors",
5
41
  "license": "MIT",
6
42
  "repository": {
7
43
  "type": "git",
8
- "url": "git+https://github.com/0xMiden/web-sdk.git",
9
- "directory": "packages/telemetry-otel"
44
+ "url": "git+https://github.com/0xMiden/web-sdk.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/0xMiden/web-sdk/issues"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json",
51
+ "lint": "eslint src",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "test:unit": "vitest run",
55
+ "test:coverage": "vitest run --coverage"
10
56
  }
11
- }
57
+ }