@hashspan/core 0.6.0 → 0.7.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 +21 -10
- package/dist/index.cjs +115 -15
- package/dist/index.d.cts +32 -19
- package/dist/index.d.mts +32 -19
- package/dist/index.mjs +114 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Payments that another party settles on chain, such as x402 payments, become a **
|
|
|
12
12
|
of a `send` span.
|
|
13
13
|
|
|
14
14
|
The core is library-agnostic and read-only: it never signs, sends or fetches anything. Adapters such as
|
|
15
|
-
[`@hashspan/viem`](https://github.com/selimaytac/hashspan/tree/@hashspan/core@0.
|
|
15
|
+
[`@hashspan/viem`](https://github.com/selimaytac/hashspan/tree/@hashspan/core@0.7.0/packages/viem) call it for you. Use the core directly to instrument any other send path.
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -69,27 +69,28 @@ stays the class name.
|
|
|
69
69
|
`tracker.startPayment({ chainId, protocol, payer, recipient, asset, amount })` records a payment that another
|
|
70
70
|
party settles on chain, such as an x402 facilitator, as a `payment {chainId}` span; end it with
|
|
71
71
|
`end({ status, hash })` or `fail(error)`. A settlement with a hash links the transaction's confirm span to the payment
|
|
72
|
-
span ([ADR 0013](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
72
|
+
span ([ADR 0013](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md)).
|
|
73
73
|
|
|
74
74
|
All three calls accept an explicit parent `Context` as a second argument. An integration that learns about a call only
|
|
75
75
|
after it started can record it after the fact: pass `startTime` in the input and `endTime` in the options of the
|
|
76
|
-
handle method, e.g. `send.end({ hash }, { endTime })` ([ADR 0009](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
76
|
+
handle method, e.g. `send.end({ hash }, { endTime })` ([ADR 0009](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0009-telemetry-off-the-call-path.md)). Every method is safe to call: failures inside
|
|
77
77
|
the instrumentation are reported through `diag` and never thrown into your code. The positional forms of earlier
|
|
78
78
|
releases, `send.end(hash, endTime)` and `send.fail(error, endTime, { errorType })`, still work and are deprecated
|
|
79
|
-
until 1.0 ([ADR 0014](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
79
|
+
until 1.0 ([ADR 0014](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md)).
|
|
80
80
|
|
|
81
81
|
## Options
|
|
82
82
|
|
|
83
83
|
| Option | Default | Description |
|
|
84
84
|
|---|---|---|
|
|
85
85
|
| `tracerProvider` | global provider | Tracer provider to use |
|
|
86
|
+
| `meterProvider` | global provider | Meter provider for the [metrics](#metrics) |
|
|
86
87
|
| `address` | `'raw'` | `'raw'`, `'hashed'`, `'off'`, or `{ mode: 'hashed', hash: (address) => string }` |
|
|
87
|
-
| `errorMessages` | `'off'` | What failed spans record about the error: `'off'` (type only), `'sanitized'` (first line, addresses per `address` mode, calldata removed; in `hashed` and `off` mode any hex longer than an address) or `'raw'` (full message and stack trace). `'raw'` can record RPC URLs that include API keys, as some libraries put the request URL in the message; `'sanitized'` keeps only the first line (viem puts the URL on a later line), which is best effort. See [ADR 0006](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
88
|
+
| `errorMessages` | `'off'` | What failed spans record about the error: `'off'` (type only), `'sanitized'` (first line, addresses per `address` mode, calldata removed; in `hashed` and `off` mode any hex longer than an address) or `'raw'` (full message and stack trace). `'raw'` can record RPC URLs that include API keys, as some libraries put the request URL in the message; `'sanitized'` keeps only the first line (viem puts the URL on a later line), which is best effort. See [ADR 0006](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0006-error-privacy.md) |
|
|
88
89
|
| `paymentResource` | `'origin'` | How much of a paid resource's URL `x402.resource` records: `'origin'` (scheme, host and port; nothing for a resource that is not a URL), `'path'` (also the path, never the query string, fragment or user info) or `'off'`. Paths often carry user or account identifiers. At most 512 characters are recorded |
|
|
89
90
|
| `recordFunctionArguments` | `false` | Record `functionArguments` as a JSON array in `blockchain.contract.function.arguments`: bigints as decimal strings, addresses per `address` mode (longer hex values become `<hex>` in `hashed` and `off` mode), at most 4096 characters. Reads only own enumerable data properties: `toJSON()` and getters are never called, so a `Date` records as `{}`; a Proxy's traps still run |
|
|
90
91
|
| `agent` | none | Agent `{ id, name }`; a field set here always wins, unset fields come from the Baggage entries `gen_ai.agent.id` / `gen_ai.agent.name` |
|
|
91
92
|
| `agentFromBaggage` | `true` | Read agent identity fields that `agent` leaves unset from Baggage; set to `false` in services that accept requests from outside their trust boundary |
|
|
92
|
-
| `redact` | none | `(attributes) => attributes`, runs last on every attribute set, including exception event attributes; if it throws, only non-sensitive identifiers are kept |
|
|
93
|
+
| `redact` | none | `(attributes) => attributes`, runs last on every span attribute set, including exception event attributes, but not on [metrics](#metrics); if it throws, only non-sensitive identifiers are kept |
|
|
93
94
|
| `linkTtlMs` | `600000` | How long a sent transaction can be linked from its confirmation |
|
|
94
95
|
| `maxTrackedTransactions` | `10000` | Upper bound on transactions kept for linking |
|
|
95
96
|
|
|
@@ -99,7 +100,17 @@ Chain id, transaction hash, sender/recipient (per `address` mode), value, nonce,
|
|
|
99
100
|
on confirmation, status, block number, gas used, effective gas price, L1 fee, total fee and revert reason. Decoded
|
|
100
101
|
call arguments are recorded only with `recordFunctionArguments`, and error messages only with `errorMessages`.
|
|
101
102
|
Attribute definitions:
|
|
102
|
-
[docs/semconv.md](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
103
|
+
[docs/semconv.md](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md).
|
|
104
|
+
|
|
105
|
+
## Metrics
|
|
106
|
+
|
|
107
|
+
With an OpenTelemetry metrics SDK set up (or `meterProvider`), the tracker records three histograms:
|
|
108
|
+
`blockchain.client.send.duration` and `blockchain.client.confirmation.duration` in seconds, and
|
|
109
|
+
`blockchain.client.fee` in wei. Their attributes are the chain and the outcome only, never an address, hash or
|
|
110
|
+
agent identity. The `redact` hook does not run on metrics: a fee it removes from spans is still recorded by
|
|
111
|
+
`blockchain.client.fee`. To keep a histogram out of your backend, drop it with a View of your metrics SDK (drop
|
|
112
|
+
aggregation). Definitions:
|
|
113
|
+
[docs/semconv.md](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md#metrics).
|
|
103
114
|
|
|
104
115
|
## Privacy notes
|
|
105
116
|
|
|
@@ -114,9 +125,9 @@ Attribute definitions:
|
|
|
114
125
|
propagated, or strip the entries before outbound calls.
|
|
115
126
|
- **Inbound Baggage can claim an identity.** A caller can send Baggage entries with any agent id. A field set in the
|
|
116
127
|
`agent` option cannot be overridden that way; to ignore identity from Baggage entirely, set `agentFromBaggage: false`
|
|
117
|
-
([ADR 0011](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
118
|
-
- The redaction hook (`redact`) runs last on every attribute set and on exception attributes; use it for
|
|
119
|
-
else your policy forbids.
|
|
128
|
+
([ADR 0011](https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0011-agent-identity-precedence.md)).
|
|
129
|
+
- The redaction hook (`redact`) runs last on every span attribute set and on exception attributes; use it for
|
|
130
|
+
anything else your policy forbids. It does not run on [metrics](#metrics), which carry no address or hash.
|
|
120
131
|
- **Your callbacks' errors go to the diagnostic logger.** If a custom `hash` function or the `redact` hook throws,
|
|
121
132
|
its error object is logged through the OpenTelemetry `diag` logger, outside the address mode and the redaction
|
|
122
133
|
hook. Errors of the instrumented call never are. Do not put sensitive values, such as the address being hashed,
|
package/dist/index.cjs
CHANGED
|
@@ -6,7 +6,7 @@ const ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name";
|
|
|
6
6
|
/**
|
|
7
7
|
* Agent identity as GenAI attributes. A field set in the static identity always wins; Baggage, which a remote caller
|
|
8
8
|
* can set, only fills fields it leaves unset, and is not read at all with `fromBaggage` false
|
|
9
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
9
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0011-agent-identity-precedence.md).
|
|
10
10
|
*/
|
|
11
11
|
function agentAttributes(ctx, identity, fromBaggage = true) {
|
|
12
12
|
const baggage = fromBaggage ? _opentelemetry_api.propagation.getBaggage(ctx) : void 0;
|
|
@@ -22,7 +22,7 @@ function agentAttributes(ctx, identity, fromBaggage = true) {
|
|
|
22
22
|
/**
|
|
23
23
|
* Attribute keys emitted by hashspan.
|
|
24
24
|
*
|
|
25
|
-
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
25
|
+
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md for
|
|
26
26
|
* definitions and value types. These names are a public contract: changes follow the deprecation policy in AGENTS.md.
|
|
27
27
|
*/
|
|
28
28
|
const ATTR_BLOCKCHAIN_SYSTEM = "blockchain.system";
|
|
@@ -46,12 +46,12 @@ const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = "blockchain.contract.function.nam
|
|
|
46
46
|
const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR = "blockchain.contract.function.selector";
|
|
47
47
|
/**
|
|
48
48
|
* Opt-in: decoded call arguments as a JSON array. See
|
|
49
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
49
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
|
|
50
50
|
*/
|
|
51
51
|
const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = "blockchain.contract.function.arguments";
|
|
52
52
|
/**
|
|
53
53
|
* Payments settled on chain by a party other than the agent, e.g. an x402 facilitator. See
|
|
54
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
54
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md.
|
|
55
55
|
*/
|
|
56
56
|
const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL = "blockchain.payment.protocol";
|
|
57
57
|
const ATTR_BLOCKCHAIN_PAYMENT_PAYER = "blockchain.payment.payer";
|
|
@@ -64,7 +64,7 @@ const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT = "blockchain.payment.settled_amoun
|
|
|
64
64
|
/**
|
|
65
65
|
* Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
|
|
66
66
|
* was possible. See
|
|
67
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
67
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0017-x402-payment-verification.md.
|
|
68
68
|
*/
|
|
69
69
|
const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED = "blockchain.payment.verified";
|
|
70
70
|
/** x402's own payment fields. */
|
|
@@ -88,7 +88,7 @@ const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = "reverted";
|
|
|
88
88
|
/**
|
|
89
89
|
* @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
|
|
90
90
|
* `blockchain.tx.status`. The constant is removed in 1.0. See
|
|
91
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
91
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
|
|
92
92
|
*/
|
|
93
93
|
const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = "timeout";
|
|
94
94
|
const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED = "replaced";
|
|
@@ -101,6 +101,83 @@ const ATTR_ERROR_TYPE = "error.type";
|
|
|
101
101
|
/** Fallback {@link ATTR_ERROR_TYPE} value when the error has no name. */
|
|
102
102
|
const ERROR_TYPE_VALUE_OTHER = "_OTHER";
|
|
103
103
|
//#endregion
|
|
104
|
+
//#region src/metrics.ts
|
|
105
|
+
/** Duration of a send: from the start of the sending call until the hash is known or the call failed. */
|
|
106
|
+
const METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION = "blockchain.client.send.duration";
|
|
107
|
+
/** Duration of a confirmation: from the start of the wait until the receipt, a timeout or a failure. */
|
|
108
|
+
const METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION = "blockchain.client.confirmation.duration";
|
|
109
|
+
/** Total fee of a mined transaction (execution fee plus L1 data fee), in the chain's smallest unit (wei). */
|
|
110
|
+
const METRIC_BLOCKCHAIN_CLIENT_FEE = "blockchain.client.fee";
|
|
111
|
+
const DURATION_BUCKETS = [
|
|
112
|
+
.05,
|
|
113
|
+
.1,
|
|
114
|
+
.25,
|
|
115
|
+
.5,
|
|
116
|
+
1,
|
|
117
|
+
2,
|
|
118
|
+
5,
|
|
119
|
+
10,
|
|
120
|
+
20,
|
|
121
|
+
30,
|
|
122
|
+
60,
|
|
123
|
+
120,
|
|
124
|
+
300
|
|
125
|
+
];
|
|
126
|
+
const FEE_BUCKETS = Array.from({ length: 11 }, (_, i) => 10 ** (i + 8));
|
|
127
|
+
/** Milliseconds since the epoch of a span time, as the OpenTelemetry API accepts it. */
|
|
128
|
+
function toEpochMs(time) {
|
|
129
|
+
if (time === void 0) return Date.now();
|
|
130
|
+
if (time instanceof Date) return time.getTime();
|
|
131
|
+
if (Array.isArray(time)) return time[0] * 1e3 + time[1] / 1e6;
|
|
132
|
+
if (typeof time !== "number") return Date.now();
|
|
133
|
+
const origin = globalThis.performance?.timeOrigin;
|
|
134
|
+
return typeof origin === "number" && time < origin ? origin + time : time;
|
|
135
|
+
}
|
|
136
|
+
function createTxMetrics(meterProvider, name, version) {
|
|
137
|
+
let histograms;
|
|
138
|
+
const get = () => {
|
|
139
|
+
histograms ??= (() => {
|
|
140
|
+
const meter = (meterProvider ?? _opentelemetry_api.metrics.getMeterProvider()).getMeter(name, version);
|
|
141
|
+
return {
|
|
142
|
+
send: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, {
|
|
143
|
+
unit: "s",
|
|
144
|
+
description: "Duration of sending a transaction, until its hash is known",
|
|
145
|
+
advice: { explicitBucketBoundaries: DURATION_BUCKETS }
|
|
146
|
+
}),
|
|
147
|
+
confirmation: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, {
|
|
148
|
+
unit: "s",
|
|
149
|
+
description: "Duration of waiting for a transaction receipt",
|
|
150
|
+
advice: { explicitBucketBoundaries: DURATION_BUCKETS }
|
|
151
|
+
}),
|
|
152
|
+
fee: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_FEE, {
|
|
153
|
+
unit: "{wei}",
|
|
154
|
+
description: "Total fee of a mined transaction",
|
|
155
|
+
advice: { explicitBucketBoundaries: FEE_BUCKETS }
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
})();
|
|
159
|
+
return histograms;
|
|
160
|
+
};
|
|
161
|
+
const record = (what, run) => {
|
|
162
|
+
try {
|
|
163
|
+
run();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
_opentelemetry_api.diag.error(`hashspan: failed to record the ${what} metric`, error);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
sendDuration: (seconds, attributes) => record("send duration", () => {
|
|
170
|
+
if (seconds >= 0) get().send.record(seconds, attributes);
|
|
171
|
+
}),
|
|
172
|
+
confirmationDuration: (seconds, attributes) => record("confirmation duration", () => {
|
|
173
|
+
if (seconds >= 0) get().confirmation.record(seconds, attributes);
|
|
174
|
+
}),
|
|
175
|
+
fee: (wei, attributes) => record("fee", () => {
|
|
176
|
+
if (wei >= 0n) get().fee.record(Number(wei), attributes);
|
|
177
|
+
})
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
104
181
|
//#region src/confirm-registry.ts
|
|
105
182
|
/**
|
|
106
183
|
* Bounded registry from (chainId, tx hash) to its in-flight confirm span, or to "settled" for a while after a
|
|
@@ -386,7 +463,7 @@ function sanitizeResource(resource) {
|
|
|
386
463
|
}
|
|
387
464
|
//#endregion
|
|
388
465
|
//#region src/version.ts
|
|
389
|
-
const VERSION = "0.
|
|
466
|
+
const VERSION = "0.7.0";
|
|
390
467
|
//#endregion
|
|
391
468
|
//#region src/tracker.ts
|
|
392
469
|
const INSTRUMENTATION_NAME = "@hashspan/core";
|
|
@@ -513,7 +590,7 @@ function reportedErrorType(error, options) {
|
|
|
513
590
|
}
|
|
514
591
|
/**
|
|
515
592
|
* Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
|
|
516
|
-
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
593
|
+
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md). It makes
|
|
517
594
|
* no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
|
|
518
595
|
* via `diag`, and a method that fails returns a handle that records nothing.
|
|
519
596
|
*/
|
|
@@ -529,6 +606,14 @@ function createTxTracker(options = {}) {
|
|
|
529
606
|
const formatAddress = safely("configure address mode", () => resolveAddressFormatter(options.address), OFF_ADDRESS_FORMATTER);
|
|
530
607
|
const errorMessages = safely("configure error message mode", () => resolveErrorMessageMode(options.errorMessages), "off");
|
|
531
608
|
const paymentResource = safely("configure payment resource mode", () => resolvePaymentResourceMode(options.paymentResource), "off");
|
|
609
|
+
const txMetrics = createTxMetrics(options.meterProvider, INSTRUMENTATION_NAME, VERSION);
|
|
610
|
+
/** Attributes of a metric: low-cardinality only, never an address, hash or agent identity. */
|
|
611
|
+
const metricAttributes = (chainId, extra = {}) => ({
|
|
612
|
+
[ATTR_BLOCKCHAIN_SYSTEM]: "evm",
|
|
613
|
+
[ATTR_BLOCKCHAIN_CHAIN_ID]: chainId,
|
|
614
|
+
...extra
|
|
615
|
+
});
|
|
616
|
+
const secondsSince = (startMs, endTime) => (toEpochMs(endTime) - startMs) / 1e3;
|
|
532
617
|
let tracer;
|
|
533
618
|
const getTracer = () => {
|
|
534
619
|
tracer ??= (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(INSTRUMENTATION_NAME, VERSION);
|
|
@@ -553,7 +638,7 @@ function createTxTracker(options = {}) {
|
|
|
553
638
|
/**
|
|
554
639
|
* Exception event attributes for `error`, per the error message mode. The error object itself is never handed to
|
|
555
640
|
* the SDK: its message and stack can carry addresses and calldata
|
|
556
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
641
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0006-error-privacy.md).
|
|
557
642
|
*/
|
|
558
643
|
const exceptionAttributes = (type, error) => {
|
|
559
644
|
const attributes = { [ATTR_EXCEPTION_TYPE]: type };
|
|
@@ -586,6 +671,7 @@ function createTxTracker(options = {}) {
|
|
|
586
671
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
587
672
|
...message !== void 0 ? { message } : {}
|
|
588
673
|
});
|
|
674
|
+
return type;
|
|
589
675
|
};
|
|
590
676
|
/** Ends a span exactly once; the span is always ended even if recording attributes fails. */
|
|
591
677
|
const finisher = (span) => {
|
|
@@ -637,6 +723,8 @@ function createTxTracker(options = {}) {
|
|
|
637
723
|
...input.startTime !== void 0 ? { startTime: input.startTime } : {}
|
|
638
724
|
}, parent);
|
|
639
725
|
const finish = finisher(span);
|
|
726
|
+
const startMs = toEpochMs(input.startTime);
|
|
727
|
+
const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), metricAttributes(input.chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
|
|
640
728
|
return {
|
|
641
729
|
context: _opentelemetry_api.trace.setSpan(parent, span),
|
|
642
730
|
end: (result, second) => finish("record transaction hash", () => {
|
|
@@ -650,10 +738,11 @@ function createTxTracker(options = {}) {
|
|
|
650
738
|
parent
|
|
651
739
|
});
|
|
652
740
|
span.setAttributes(redact({ [ATTR_BLOCKCHAIN_TX_HASH]: hash }));
|
|
741
|
+
recordSend(handleOptions(second).endTime);
|
|
653
742
|
}, handleOptions(second).endTime),
|
|
654
743
|
fail: (error, second, third) => {
|
|
655
744
|
const options = handleOptions(second, third);
|
|
656
|
-
finish("record send failure", () => markError(span, reportedErrorType(error, options), error, errorType(error)), options.endTime);
|
|
745
|
+
finish("record send failure", () => recordSend(options.endTime, markError(span, reportedErrorType(error, options), error, errorType(error))), options.endTime);
|
|
657
746
|
}
|
|
658
747
|
};
|
|
659
748
|
};
|
|
@@ -689,6 +778,8 @@ function createTxTracker(options = {}) {
|
|
|
689
778
|
...explicitStart !== void 0 ? { startTime: explicitStart } : {}
|
|
690
779
|
}, parent);
|
|
691
780
|
const finish = finisher(span);
|
|
781
|
+
const startMs = toEpochMs(explicitStart);
|
|
782
|
+
const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), metricAttributes(input.chainId, outcome));
|
|
692
783
|
return {
|
|
693
784
|
active: 0,
|
|
694
785
|
ended: false,
|
|
@@ -698,11 +789,16 @@ function createTxTracker(options = {}) {
|
|
|
698
789
|
links: [{ context: span.spanContext() }, ...sent ? [{ context: sent.spanContext }] : []]
|
|
699
790
|
},
|
|
700
791
|
receipt: (receipt, endTime) => finish("record receipt", () => {
|
|
701
|
-
|
|
792
|
+
const attributes = receiptAttributes(receipt);
|
|
793
|
+
span.setAttributes(redact(attributes));
|
|
702
794
|
if (receipt.status === "reverted") markError(span, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED);
|
|
795
|
+
const status = { [ATTR_BLOCKCHAIN_TX_STATUS]: attributes[ATTR_BLOCKCHAIN_TX_STATUS] };
|
|
796
|
+
recordConfirmation(endTime, status);
|
|
797
|
+
const fee = attributes[ATTR_BLOCKCHAIN_TX_FEE];
|
|
798
|
+
if (typeof fee === "string") txMetrics.fee(BigInt(fee), metricAttributes(input.chainId, status));
|
|
703
799
|
}, endTime),
|
|
704
|
-
timeout: (endTime) => finish("record confirmation timeout", () => markError(span, OBSERVER_TIMEOUT), endTime),
|
|
705
|
-
fail: (error, endTime) => finish("record confirmation failure", () => markError(span, errorType(error), error), endTime),
|
|
800
|
+
timeout: (endTime) => finish("record confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
|
|
801
|
+
fail: (error, endTime) => finish("record confirmation failure", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, errorType(error), error) }), endTime),
|
|
706
802
|
replaced: (hash, reason, endTime) => finish("record replacement", () => {
|
|
707
803
|
const attributes = {
|
|
708
804
|
[ATTR_BLOCKCHAIN_TX_STATUS]: BLOCKCHAIN_TX_STATUS_VALUE_REPLACED,
|
|
@@ -710,8 +806,9 @@ function createTxTracker(options = {}) {
|
|
|
710
806
|
};
|
|
711
807
|
if (reason !== void 0 && REPLACEMENT_REASONS.has(reason)) attributes[ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON] = reason;
|
|
712
808
|
span.setAttributes(redact(attributes));
|
|
809
|
+
recordConfirmation(endTime, { [ATTR_BLOCKCHAIN_TX_STATUS]: BLOCKCHAIN_TX_STATUS_VALUE_REPLACED });
|
|
713
810
|
}, endTime),
|
|
714
|
-
unattributable: (endTime) => finish("record unattributable receipt", () => markError(span, ERROR_TYPE_VALUE_OTHER), endTime)
|
|
811
|
+
unattributable: (endTime) => finish("record unattributable receipt", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, ERROR_TYPE_VALUE_OTHER) }), endTime)
|
|
715
812
|
};
|
|
716
813
|
};
|
|
717
814
|
/**
|
|
@@ -733,7 +830,7 @@ function createTxTracker(options = {}) {
|
|
|
733
830
|
};
|
|
734
831
|
/**
|
|
735
832
|
* Ends `shared` with `receipt`, attributing it to the transaction that was mined
|
|
736
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
833
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0008-replaced-transactions.md).
|
|
737
834
|
*/
|
|
738
835
|
const endWithReceipt = (chainId, hash, shared, receipt, endTime) => {
|
|
739
836
|
const mined = receipt.transactionHash;
|
|
@@ -918,5 +1015,8 @@ exports.BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = BLOCKCHAIN_TX_STATUS_VALUE_REVERTE
|
|
|
918
1015
|
exports.BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS = BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS;
|
|
919
1016
|
exports.BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT;
|
|
920
1017
|
exports.ERROR_TYPE_VALUE_OTHER = ERROR_TYPE_VALUE_OTHER;
|
|
1018
|
+
exports.METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION = METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION;
|
|
1019
|
+
exports.METRIC_BLOCKCHAIN_CLIENT_FEE = METRIC_BLOCKCHAIN_CLIENT_FEE;
|
|
1020
|
+
exports.METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION = METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION;
|
|
921
1021
|
exports.VERSION = VERSION;
|
|
922
1022
|
exports.createTxTracker = createTxTracker;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Attributes, Context, TimeInput, TracerProvider } from "@opentelemetry/api";
|
|
1
|
+
import { Attributes, Context, MeterProvider, TimeInput, TracerProvider } from "@opentelemetry/api";
|
|
2
2
|
//#region src/types.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* How wallet addresses are recorded. See
|
|
5
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
5
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
|
|
6
6
|
*/
|
|
7
7
|
type AddressMode = "raw" | "hashed" | "off";
|
|
8
8
|
/**
|
|
9
9
|
* How error messages are recorded on exception events and span status. See
|
|
10
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
10
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0006-error-privacy.md.
|
|
11
11
|
* - `off`: error type only
|
|
12
12
|
* - `sanitized`: first line, addresses per address mode, other long hex data removed
|
|
13
13
|
* - `raw`: full message and stack trace, as thrown
|
|
@@ -43,6 +43,11 @@ interface AgentIdentity {
|
|
|
43
43
|
interface TxTrackerOptions {
|
|
44
44
|
/** Defaults to the globally registered tracer provider. */
|
|
45
45
|
tracerProvider?: TracerProvider | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Meter provider for the send, confirmation and fee histograms. Defaults to the globally registered one, which
|
|
48
|
+
* records nothing until an OpenTelemetry metrics SDK is set up.
|
|
49
|
+
*/
|
|
50
|
+
meterProvider?: MeterProvider | undefined;
|
|
46
51
|
/** Address recording mode. Default: `raw`. */
|
|
47
52
|
address?: AddressMode | AddressOptions | undefined;
|
|
48
53
|
/**
|
|
@@ -61,7 +66,7 @@ interface TxTrackerOptions {
|
|
|
61
66
|
/**
|
|
62
67
|
* Agent identity. A field set here always wins; fields left unset are taken from the Baggage entries
|
|
63
68
|
* `gen_ai.agent.id` / `gen_ai.agent.name` unless `agentFromBaggage` is false
|
|
64
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
69
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0011-agent-identity-precedence.md).
|
|
65
70
|
*/
|
|
66
71
|
agent?: AgentIdentity | undefined;
|
|
67
72
|
/**
|
|
@@ -103,7 +108,7 @@ interface SendInput {
|
|
|
103
108
|
functionArguments?: readonly unknown[] | undefined;
|
|
104
109
|
/**
|
|
105
110
|
* When the send started, for adapters that record it after the fact
|
|
106
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
111
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0009-telemetry-off-the-call-path.md).
|
|
107
112
|
* Omit it otherwise: with an explicit start time, the SDK measures the span by the wall clock, so pass the end time
|
|
108
113
|
* to the handle too.
|
|
109
114
|
*/
|
|
@@ -112,14 +117,14 @@ interface SendInput {
|
|
|
112
117
|
/**
|
|
113
118
|
* Ends a send span. Only the first call counts; methods never throw.
|
|
114
119
|
* Produced by the tracker only; methods may be added in minor releases
|
|
115
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
120
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
116
121
|
*/
|
|
117
122
|
interface SendHandle {
|
|
118
123
|
/**
|
|
119
124
|
* The parent context with the send span set. Run the call that sends the transaction in it, e.g.
|
|
120
125
|
* `await context.with(send.context, () => sendSomehow())`, so that spans of wallet, RPC or HTTP instrumentation
|
|
121
126
|
* nest under the send span
|
|
122
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
127
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0015-send-span-as-active-context.md).
|
|
123
128
|
* Run only that call in it: a confirm span started in it becomes a child of the send span.
|
|
124
129
|
*/
|
|
125
130
|
readonly context: Context;
|
|
@@ -182,7 +187,7 @@ interface ReceiptLike {
|
|
|
182
187
|
/**
|
|
183
188
|
* Hash of the mined transaction. When it differs from the awaited hash, the awaited transaction was replaced: its
|
|
184
189
|
* confirm span ends as `replaced` and the receipt is recorded for this hash
|
|
185
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
190
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0008-replaced-transactions.md).
|
|
186
191
|
*/
|
|
187
192
|
transactionHash?: string | undefined;
|
|
188
193
|
/** Replacement reason reported by the library, when {@link transactionHash} differs from the awaited hash. */
|
|
@@ -192,7 +197,7 @@ interface ReceiptLike {
|
|
|
192
197
|
* One wait for a transaction's receipt, joined to the transaction's shared confirm span. Only the first call counts;
|
|
193
198
|
* methods never throw.
|
|
194
199
|
* Produced by the tracker only; methods may be added in minor releases
|
|
195
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
200
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
196
201
|
*/
|
|
197
202
|
interface ConfirmHandle {
|
|
198
203
|
/** Ends the shared confirm span with the receipt, for every handle of the transaction. */
|
|
@@ -216,7 +221,7 @@ interface ConfirmHandle {
|
|
|
216
221
|
}
|
|
217
222
|
/**
|
|
218
223
|
* A payment the agent authorizes and another party settles on chain, e.g. an x402 facilitator
|
|
219
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
224
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md).
|
|
220
225
|
* Values often come from a remote server: addresses, amounts and identifiers that are malformed are not recorded.
|
|
221
226
|
*/
|
|
222
227
|
interface PaymentInput {
|
|
@@ -268,7 +273,7 @@ interface PaymentSettlement {
|
|
|
268
273
|
/**
|
|
269
274
|
* Ends a payment span. Only the first call counts; methods never throw.
|
|
270
275
|
* Produced by the tracker only; methods may be added in minor releases
|
|
271
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
276
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
272
277
|
*/
|
|
273
278
|
interface PaymentHandle {
|
|
274
279
|
/** Ends the payment span with its settlement. */
|
|
@@ -300,7 +305,7 @@ export declare const ATTR_GEN_AI_AGENT_NAME: "gen_ai.agent.name";
|
|
|
300
305
|
/**
|
|
301
306
|
* Attribute keys emitted by hashspan.
|
|
302
307
|
*
|
|
303
|
-
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
308
|
+
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md for
|
|
304
309
|
* definitions and value types. These names are a public contract: changes follow the deprecation policy in AGENTS.md.
|
|
305
310
|
*/
|
|
306
311
|
export declare const ATTR_BLOCKCHAIN_SYSTEM: "blockchain.system";
|
|
@@ -324,12 +329,12 @@ export declare const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME: "blockchain.contrac
|
|
|
324
329
|
export declare const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR: "blockchain.contract.function.selector";
|
|
325
330
|
/**
|
|
326
331
|
* Opt-in: decoded call arguments as a JSON array. See
|
|
327
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
332
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
|
|
328
333
|
*/
|
|
329
334
|
export declare const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS: "blockchain.contract.function.arguments";
|
|
330
335
|
/**
|
|
331
336
|
* Payments settled on chain by a party other than the agent, e.g. an x402 facilitator. See
|
|
332
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
337
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md.
|
|
333
338
|
*/
|
|
334
339
|
export declare const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL: "blockchain.payment.protocol";
|
|
335
340
|
export declare const ATTR_BLOCKCHAIN_PAYMENT_PAYER: "blockchain.payment.payer";
|
|
@@ -342,7 +347,7 @@ export declare const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT: "blockchain.payment
|
|
|
342
347
|
/**
|
|
343
348
|
* Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
|
|
344
349
|
* was possible. See
|
|
345
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
350
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0017-x402-payment-verification.md.
|
|
346
351
|
*/
|
|
347
352
|
export declare const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED: "blockchain.payment.verified";
|
|
348
353
|
/** x402's own payment fields. */
|
|
@@ -366,7 +371,7 @@ export declare const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED: "reverted";
|
|
|
366
371
|
/**
|
|
367
372
|
* @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
|
|
368
373
|
* `blockchain.tx.status`. The constant is removed in 1.0. See
|
|
369
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
374
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
|
|
370
375
|
*/
|
|
371
376
|
export declare const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT: "timeout";
|
|
372
377
|
export declare const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED: "replaced";
|
|
@@ -379,11 +384,19 @@ export declare const ATTR_ERROR_TYPE: "error.type";
|
|
|
379
384
|
/** Fallback {@link ATTR_ERROR_TYPE} value when the error has no name. */
|
|
380
385
|
export declare const ERROR_TYPE_VALUE_OTHER: "_OTHER";
|
|
381
386
|
//#endregion
|
|
387
|
+
//#region src/metrics.d.ts
|
|
388
|
+
/** Duration of a send: from the start of the sending call until the hash is known or the call failed. */
|
|
389
|
+
export declare const METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION: "blockchain.client.send.duration";
|
|
390
|
+
/** Duration of a confirmation: from the start of the wait until the receipt, a timeout or a failure. */
|
|
391
|
+
export declare const METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION: "blockchain.client.confirmation.duration";
|
|
392
|
+
/** Total fee of a mined transaction (execution fee plus L1 data fee), in the chain's smallest unit (wei). */
|
|
393
|
+
export declare const METRIC_BLOCKCHAIN_CLIENT_FEE: "blockchain.client.fee";
|
|
394
|
+
//#endregion
|
|
382
395
|
//#region src/tracker.d.ts
|
|
383
396
|
/**
|
|
384
397
|
* Records transactions and payments as spans. Obtain one from {@link createTxTracker}: it is not meant to be
|
|
385
398
|
* implemented, and members may be added to it and to its handles in minor releases
|
|
386
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
399
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
387
400
|
*/
|
|
388
401
|
interface TxTracker {
|
|
389
402
|
/**
|
|
@@ -401,7 +414,7 @@ interface TxTracker {
|
|
|
401
414
|
/**
|
|
402
415
|
* Starts a `payment` span as a child of `parent` (default: the active context), for a payment that another party
|
|
403
416
|
* settles on chain
|
|
404
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
417
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md). Call
|
|
405
418
|
* `end(settlement)` with the settlement, or `fail(error)`. A settlement with a hash links the transaction's confirm
|
|
406
419
|
* span to this span, as a send span would.
|
|
407
420
|
*/
|
|
@@ -409,7 +422,7 @@ interface TxTracker {
|
|
|
409
422
|
}
|
|
410
423
|
/**
|
|
411
424
|
* Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
|
|
412
|
-
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
425
|
+
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md). It makes
|
|
413
426
|
* no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
|
|
414
427
|
* via `diag`, and a method that fails returns a handle that records nothing.
|
|
415
428
|
*/
|
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Attributes, Context, TimeInput, TracerProvider } from "@opentelemetry/api";
|
|
1
|
+
import { Attributes, Context, MeterProvider, TimeInput, TracerProvider } from "@opentelemetry/api";
|
|
2
2
|
//#region src/types.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* How wallet addresses are recorded. See
|
|
5
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
5
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
|
|
6
6
|
*/
|
|
7
7
|
type AddressMode = "raw" | "hashed" | "off";
|
|
8
8
|
/**
|
|
9
9
|
* How error messages are recorded on exception events and span status. See
|
|
10
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
10
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0006-error-privacy.md.
|
|
11
11
|
* - `off`: error type only
|
|
12
12
|
* - `sanitized`: first line, addresses per address mode, other long hex data removed
|
|
13
13
|
* - `raw`: full message and stack trace, as thrown
|
|
@@ -43,6 +43,11 @@ interface AgentIdentity {
|
|
|
43
43
|
interface TxTrackerOptions {
|
|
44
44
|
/** Defaults to the globally registered tracer provider. */
|
|
45
45
|
tracerProvider?: TracerProvider | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Meter provider for the send, confirmation and fee histograms. Defaults to the globally registered one, which
|
|
48
|
+
* records nothing until an OpenTelemetry metrics SDK is set up.
|
|
49
|
+
*/
|
|
50
|
+
meterProvider?: MeterProvider | undefined;
|
|
46
51
|
/** Address recording mode. Default: `raw`. */
|
|
47
52
|
address?: AddressMode | AddressOptions | undefined;
|
|
48
53
|
/**
|
|
@@ -61,7 +66,7 @@ interface TxTrackerOptions {
|
|
|
61
66
|
/**
|
|
62
67
|
* Agent identity. A field set here always wins; fields left unset are taken from the Baggage entries
|
|
63
68
|
* `gen_ai.agent.id` / `gen_ai.agent.name` unless `agentFromBaggage` is false
|
|
64
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
69
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0011-agent-identity-precedence.md).
|
|
65
70
|
*/
|
|
66
71
|
agent?: AgentIdentity | undefined;
|
|
67
72
|
/**
|
|
@@ -103,7 +108,7 @@ interface SendInput {
|
|
|
103
108
|
functionArguments?: readonly unknown[] | undefined;
|
|
104
109
|
/**
|
|
105
110
|
* When the send started, for adapters that record it after the fact
|
|
106
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
111
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0009-telemetry-off-the-call-path.md).
|
|
107
112
|
* Omit it otherwise: with an explicit start time, the SDK measures the span by the wall clock, so pass the end time
|
|
108
113
|
* to the handle too.
|
|
109
114
|
*/
|
|
@@ -112,14 +117,14 @@ interface SendInput {
|
|
|
112
117
|
/**
|
|
113
118
|
* Ends a send span. Only the first call counts; methods never throw.
|
|
114
119
|
* Produced by the tracker only; methods may be added in minor releases
|
|
115
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
120
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
116
121
|
*/
|
|
117
122
|
interface SendHandle {
|
|
118
123
|
/**
|
|
119
124
|
* The parent context with the send span set. Run the call that sends the transaction in it, e.g.
|
|
120
125
|
* `await context.with(send.context, () => sendSomehow())`, so that spans of wallet, RPC or HTTP instrumentation
|
|
121
126
|
* nest under the send span
|
|
122
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
127
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0015-send-span-as-active-context.md).
|
|
123
128
|
* Run only that call in it: a confirm span started in it becomes a child of the send span.
|
|
124
129
|
*/
|
|
125
130
|
readonly context: Context;
|
|
@@ -182,7 +187,7 @@ interface ReceiptLike {
|
|
|
182
187
|
/**
|
|
183
188
|
* Hash of the mined transaction. When it differs from the awaited hash, the awaited transaction was replaced: its
|
|
184
189
|
* confirm span ends as `replaced` and the receipt is recorded for this hash
|
|
185
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
190
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0008-replaced-transactions.md).
|
|
186
191
|
*/
|
|
187
192
|
transactionHash?: string | undefined;
|
|
188
193
|
/** Replacement reason reported by the library, when {@link transactionHash} differs from the awaited hash. */
|
|
@@ -192,7 +197,7 @@ interface ReceiptLike {
|
|
|
192
197
|
* One wait for a transaction's receipt, joined to the transaction's shared confirm span. Only the first call counts;
|
|
193
198
|
* methods never throw.
|
|
194
199
|
* Produced by the tracker only; methods may be added in minor releases
|
|
195
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
200
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
196
201
|
*/
|
|
197
202
|
interface ConfirmHandle {
|
|
198
203
|
/** Ends the shared confirm span with the receipt, for every handle of the transaction. */
|
|
@@ -216,7 +221,7 @@ interface ConfirmHandle {
|
|
|
216
221
|
}
|
|
217
222
|
/**
|
|
218
223
|
* A payment the agent authorizes and another party settles on chain, e.g. an x402 facilitator
|
|
219
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
224
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md).
|
|
220
225
|
* Values often come from a remote server: addresses, amounts and identifiers that are malformed are not recorded.
|
|
221
226
|
*/
|
|
222
227
|
interface PaymentInput {
|
|
@@ -268,7 +273,7 @@ interface PaymentSettlement {
|
|
|
268
273
|
/**
|
|
269
274
|
* Ends a payment span. Only the first call counts; methods never throw.
|
|
270
275
|
* Produced by the tracker only; methods may be added in minor releases
|
|
271
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
276
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
272
277
|
*/
|
|
273
278
|
interface PaymentHandle {
|
|
274
279
|
/** Ends the payment span with its settlement. */
|
|
@@ -300,7 +305,7 @@ export declare const ATTR_GEN_AI_AGENT_NAME: "gen_ai.agent.name";
|
|
|
300
305
|
/**
|
|
301
306
|
* Attribute keys emitted by hashspan.
|
|
302
307
|
*
|
|
303
|
-
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
308
|
+
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md for
|
|
304
309
|
* definitions and value types. These names are a public contract: changes follow the deprecation policy in AGENTS.md.
|
|
305
310
|
*/
|
|
306
311
|
export declare const ATTR_BLOCKCHAIN_SYSTEM: "blockchain.system";
|
|
@@ -324,12 +329,12 @@ export declare const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME: "blockchain.contrac
|
|
|
324
329
|
export declare const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR: "blockchain.contract.function.selector";
|
|
325
330
|
/**
|
|
326
331
|
* Opt-in: decoded call arguments as a JSON array. See
|
|
327
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
332
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
|
|
328
333
|
*/
|
|
329
334
|
export declare const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS: "blockchain.contract.function.arguments";
|
|
330
335
|
/**
|
|
331
336
|
* Payments settled on chain by a party other than the agent, e.g. an x402 facilitator. See
|
|
332
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
337
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md.
|
|
333
338
|
*/
|
|
334
339
|
export declare const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL: "blockchain.payment.protocol";
|
|
335
340
|
export declare const ATTR_BLOCKCHAIN_PAYMENT_PAYER: "blockchain.payment.payer";
|
|
@@ -342,7 +347,7 @@ export declare const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT: "blockchain.payment
|
|
|
342
347
|
/**
|
|
343
348
|
* Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
|
|
344
349
|
* was possible. See
|
|
345
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
350
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0017-x402-payment-verification.md.
|
|
346
351
|
*/
|
|
347
352
|
export declare const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED: "blockchain.payment.verified";
|
|
348
353
|
/** x402's own payment fields. */
|
|
@@ -366,7 +371,7 @@ export declare const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED: "reverted";
|
|
|
366
371
|
/**
|
|
367
372
|
* @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
|
|
368
373
|
* `blockchain.tx.status`. The constant is removed in 1.0. See
|
|
369
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
374
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
|
|
370
375
|
*/
|
|
371
376
|
export declare const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT: "timeout";
|
|
372
377
|
export declare const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED: "replaced";
|
|
@@ -379,11 +384,19 @@ export declare const ATTR_ERROR_TYPE: "error.type";
|
|
|
379
384
|
/** Fallback {@link ATTR_ERROR_TYPE} value when the error has no name. */
|
|
380
385
|
export declare const ERROR_TYPE_VALUE_OTHER: "_OTHER";
|
|
381
386
|
//#endregion
|
|
387
|
+
//#region src/metrics.d.ts
|
|
388
|
+
/** Duration of a send: from the start of the sending call until the hash is known or the call failed. */
|
|
389
|
+
export declare const METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION: "blockchain.client.send.duration";
|
|
390
|
+
/** Duration of a confirmation: from the start of the wait until the receipt, a timeout or a failure. */
|
|
391
|
+
export declare const METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION: "blockchain.client.confirmation.duration";
|
|
392
|
+
/** Total fee of a mined transaction (execution fee plus L1 data fee), in the chain's smallest unit (wei). */
|
|
393
|
+
export declare const METRIC_BLOCKCHAIN_CLIENT_FEE: "blockchain.client.fee";
|
|
394
|
+
//#endregion
|
|
382
395
|
//#region src/tracker.d.ts
|
|
383
396
|
/**
|
|
384
397
|
* Records transactions and payments as spans. Obtain one from {@link createTxTracker}: it is not meant to be
|
|
385
398
|
* implemented, and members may be added to it and to its handles in minor releases
|
|
386
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
399
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0014-core-api-boundary.md).
|
|
387
400
|
*/
|
|
388
401
|
interface TxTracker {
|
|
389
402
|
/**
|
|
@@ -401,7 +414,7 @@ interface TxTracker {
|
|
|
401
414
|
/**
|
|
402
415
|
* Starts a `payment` span as a child of `parent` (default: the active context), for a payment that another party
|
|
403
416
|
* settles on chain
|
|
404
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
417
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md). Call
|
|
405
418
|
* `end(settlement)` with the settlement, or `fail(error)`. A settlement with a hash links the transaction's confirm
|
|
406
419
|
* span to this span, as a send span would.
|
|
407
420
|
*/
|
|
@@ -409,7 +422,7 @@ interface TxTracker {
|
|
|
409
422
|
}
|
|
410
423
|
/**
|
|
411
424
|
* Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
|
|
412
|
-
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
425
|
+
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md). It makes
|
|
413
426
|
* no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
|
|
414
427
|
* via `diag`, and a method that fails returns a handle that records nothing.
|
|
415
428
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { SpanKind, SpanStatusCode, context, diag, propagation, trace } from "@opentelemetry/api";
|
|
1
|
+
import { SpanKind, SpanStatusCode, context, diag, metrics, propagation, trace } from "@opentelemetry/api";
|
|
2
2
|
//#region src/agent.ts
|
|
3
3
|
const ATTR_GEN_AI_AGENT_ID = "gen_ai.agent.id";
|
|
4
4
|
const ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name";
|
|
5
5
|
/**
|
|
6
6
|
* Agent identity as GenAI attributes. A field set in the static identity always wins; Baggage, which a remote caller
|
|
7
7
|
* can set, only fills fields it leaves unset, and is not read at all with `fromBaggage` false
|
|
8
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
8
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0011-agent-identity-precedence.md).
|
|
9
9
|
*/
|
|
10
10
|
function agentAttributes(ctx, identity, fromBaggage = true) {
|
|
11
11
|
const baggage = fromBaggage ? propagation.getBaggage(ctx) : void 0;
|
|
@@ -21,7 +21,7 @@ function agentAttributes(ctx, identity, fromBaggage = true) {
|
|
|
21
21
|
/**
|
|
22
22
|
* Attribute keys emitted by hashspan.
|
|
23
23
|
*
|
|
24
|
-
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
24
|
+
* Stability: development. See https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md for
|
|
25
25
|
* definitions and value types. These names are a public contract: changes follow the deprecation policy in AGENTS.md.
|
|
26
26
|
*/
|
|
27
27
|
const ATTR_BLOCKCHAIN_SYSTEM = "blockchain.system";
|
|
@@ -45,12 +45,12 @@ const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME = "blockchain.contract.function.nam
|
|
|
45
45
|
const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR = "blockchain.contract.function.selector";
|
|
46
46
|
/**
|
|
47
47
|
* Opt-in: decoded call arguments as a JSON array. See
|
|
48
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
48
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0004-privacy-defaults.md.
|
|
49
49
|
*/
|
|
50
50
|
const ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS = "blockchain.contract.function.arguments";
|
|
51
51
|
/**
|
|
52
52
|
* Payments settled on chain by a party other than the agent, e.g. an x402 facilitator. See
|
|
53
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
53
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0013-x402-payments.md.
|
|
54
54
|
*/
|
|
55
55
|
const ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL = "blockchain.payment.protocol";
|
|
56
56
|
const ATTR_BLOCKCHAIN_PAYMENT_PAYER = "blockchain.payment.payer";
|
|
@@ -63,7 +63,7 @@ const ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT = "blockchain.payment.settled_amoun
|
|
|
63
63
|
/**
|
|
64
64
|
* Whether the settlement transaction's receipt carries the payment, as checked by the adapter; absent when no check
|
|
65
65
|
* was possible. See
|
|
66
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
66
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0017-x402-payment-verification.md.
|
|
67
67
|
*/
|
|
68
68
|
const ATTR_BLOCKCHAIN_PAYMENT_VERIFIED = "blockchain.payment.verified";
|
|
69
69
|
/** x402's own payment fields. */
|
|
@@ -87,7 +87,7 @@ const BLOCKCHAIN_TX_STATUS_VALUE_REVERTED = "reverted";
|
|
|
87
87
|
/**
|
|
88
88
|
* @deprecated No longer recorded: a confirm span that gave up waiting records `error.type` `timeout` and no
|
|
89
89
|
* `blockchain.tx.status`. The constant is removed in 1.0. See
|
|
90
|
-
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
90
|
+
* https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0016-timeout-is-an-observer-outcome.md.
|
|
91
91
|
*/
|
|
92
92
|
const BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT = "timeout";
|
|
93
93
|
const BLOCKCHAIN_TX_STATUS_VALUE_REPLACED = "replaced";
|
|
@@ -100,6 +100,83 @@ const ATTR_ERROR_TYPE = "error.type";
|
|
|
100
100
|
/** Fallback {@link ATTR_ERROR_TYPE} value when the error has no name. */
|
|
101
101
|
const ERROR_TYPE_VALUE_OTHER = "_OTHER";
|
|
102
102
|
//#endregion
|
|
103
|
+
//#region src/metrics.ts
|
|
104
|
+
/** Duration of a send: from the start of the sending call until the hash is known or the call failed. */
|
|
105
|
+
const METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION = "blockchain.client.send.duration";
|
|
106
|
+
/** Duration of a confirmation: from the start of the wait until the receipt, a timeout or a failure. */
|
|
107
|
+
const METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION = "blockchain.client.confirmation.duration";
|
|
108
|
+
/** Total fee of a mined transaction (execution fee plus L1 data fee), in the chain's smallest unit (wei). */
|
|
109
|
+
const METRIC_BLOCKCHAIN_CLIENT_FEE = "blockchain.client.fee";
|
|
110
|
+
const DURATION_BUCKETS = [
|
|
111
|
+
.05,
|
|
112
|
+
.1,
|
|
113
|
+
.25,
|
|
114
|
+
.5,
|
|
115
|
+
1,
|
|
116
|
+
2,
|
|
117
|
+
5,
|
|
118
|
+
10,
|
|
119
|
+
20,
|
|
120
|
+
30,
|
|
121
|
+
60,
|
|
122
|
+
120,
|
|
123
|
+
300
|
|
124
|
+
];
|
|
125
|
+
const FEE_BUCKETS = Array.from({ length: 11 }, (_, i) => 10 ** (i + 8));
|
|
126
|
+
/** Milliseconds since the epoch of a span time, as the OpenTelemetry API accepts it. */
|
|
127
|
+
function toEpochMs(time) {
|
|
128
|
+
if (time === void 0) return Date.now();
|
|
129
|
+
if (time instanceof Date) return time.getTime();
|
|
130
|
+
if (Array.isArray(time)) return time[0] * 1e3 + time[1] / 1e6;
|
|
131
|
+
if (typeof time !== "number") return Date.now();
|
|
132
|
+
const origin = globalThis.performance?.timeOrigin;
|
|
133
|
+
return typeof origin === "number" && time < origin ? origin + time : time;
|
|
134
|
+
}
|
|
135
|
+
function createTxMetrics(meterProvider, name, version) {
|
|
136
|
+
let histograms;
|
|
137
|
+
const get = () => {
|
|
138
|
+
histograms ??= (() => {
|
|
139
|
+
const meter = (meterProvider ?? metrics.getMeterProvider()).getMeter(name, version);
|
|
140
|
+
return {
|
|
141
|
+
send: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, {
|
|
142
|
+
unit: "s",
|
|
143
|
+
description: "Duration of sending a transaction, until its hash is known",
|
|
144
|
+
advice: { explicitBucketBoundaries: DURATION_BUCKETS }
|
|
145
|
+
}),
|
|
146
|
+
confirmation: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, {
|
|
147
|
+
unit: "s",
|
|
148
|
+
description: "Duration of waiting for a transaction receipt",
|
|
149
|
+
advice: { explicitBucketBoundaries: DURATION_BUCKETS }
|
|
150
|
+
}),
|
|
151
|
+
fee: meter.createHistogram(METRIC_BLOCKCHAIN_CLIENT_FEE, {
|
|
152
|
+
unit: "{wei}",
|
|
153
|
+
description: "Total fee of a mined transaction",
|
|
154
|
+
advice: { explicitBucketBoundaries: FEE_BUCKETS }
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
})();
|
|
158
|
+
return histograms;
|
|
159
|
+
};
|
|
160
|
+
const record = (what, run) => {
|
|
161
|
+
try {
|
|
162
|
+
run();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
diag.error(`hashspan: failed to record the ${what} metric`, error);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
sendDuration: (seconds, attributes) => record("send duration", () => {
|
|
169
|
+
if (seconds >= 0) get().send.record(seconds, attributes);
|
|
170
|
+
}),
|
|
171
|
+
confirmationDuration: (seconds, attributes) => record("confirmation duration", () => {
|
|
172
|
+
if (seconds >= 0) get().confirmation.record(seconds, attributes);
|
|
173
|
+
}),
|
|
174
|
+
fee: (wei, attributes) => record("fee", () => {
|
|
175
|
+
if (wei >= 0n) get().fee.record(Number(wei), attributes);
|
|
176
|
+
})
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
103
180
|
//#region src/confirm-registry.ts
|
|
104
181
|
/**
|
|
105
182
|
* Bounded registry from (chainId, tx hash) to its in-flight confirm span, or to "settled" for a while after a
|
|
@@ -385,7 +462,7 @@ function sanitizeResource(resource) {
|
|
|
385
462
|
}
|
|
386
463
|
//#endregion
|
|
387
464
|
//#region src/version.ts
|
|
388
|
-
const VERSION = "0.
|
|
465
|
+
const VERSION = "0.7.0";
|
|
389
466
|
//#endregion
|
|
390
467
|
//#region src/tracker.ts
|
|
391
468
|
const INSTRUMENTATION_NAME = "@hashspan/core";
|
|
@@ -512,7 +589,7 @@ function reportedErrorType(error, options) {
|
|
|
512
589
|
}
|
|
513
590
|
/**
|
|
514
591
|
* Creates a tracker that records transactions as `send` and `confirm` spans, and payments as `payment` spans, with
|
|
515
|
-
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
592
|
+
* `@opentelemetry/api` (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/semconv.md). It makes
|
|
516
593
|
* no network calls; the caller passes hashes and receipts. Its methods and handles never throw: failures are logged
|
|
517
594
|
* via `diag`, and a method that fails returns a handle that records nothing.
|
|
518
595
|
*/
|
|
@@ -528,6 +605,14 @@ function createTxTracker(options = {}) {
|
|
|
528
605
|
const formatAddress = safely("configure address mode", () => resolveAddressFormatter(options.address), OFF_ADDRESS_FORMATTER);
|
|
529
606
|
const errorMessages = safely("configure error message mode", () => resolveErrorMessageMode(options.errorMessages), "off");
|
|
530
607
|
const paymentResource = safely("configure payment resource mode", () => resolvePaymentResourceMode(options.paymentResource), "off");
|
|
608
|
+
const txMetrics = createTxMetrics(options.meterProvider, INSTRUMENTATION_NAME, VERSION);
|
|
609
|
+
/** Attributes of a metric: low-cardinality only, never an address, hash or agent identity. */
|
|
610
|
+
const metricAttributes = (chainId, extra = {}) => ({
|
|
611
|
+
[ATTR_BLOCKCHAIN_SYSTEM]: "evm",
|
|
612
|
+
[ATTR_BLOCKCHAIN_CHAIN_ID]: chainId,
|
|
613
|
+
...extra
|
|
614
|
+
});
|
|
615
|
+
const secondsSince = (startMs, endTime) => (toEpochMs(endTime) - startMs) / 1e3;
|
|
531
616
|
let tracer;
|
|
532
617
|
const getTracer = () => {
|
|
533
618
|
tracer ??= (options.tracerProvider ?? trace.getTracerProvider()).getTracer(INSTRUMENTATION_NAME, VERSION);
|
|
@@ -552,7 +637,7 @@ function createTxTracker(options = {}) {
|
|
|
552
637
|
/**
|
|
553
638
|
* Exception event attributes for `error`, per the error message mode. The error object itself is never handed to
|
|
554
639
|
* the SDK: its message and stack can carry addresses and calldata
|
|
555
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
640
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0006-error-privacy.md).
|
|
556
641
|
*/
|
|
557
642
|
const exceptionAttributes = (type, error) => {
|
|
558
643
|
const attributes = { [ATTR_EXCEPTION_TYPE]: type };
|
|
@@ -585,6 +670,7 @@ function createTxTracker(options = {}) {
|
|
|
585
670
|
code: SpanStatusCode.ERROR,
|
|
586
671
|
...message !== void 0 ? { message } : {}
|
|
587
672
|
});
|
|
673
|
+
return type;
|
|
588
674
|
};
|
|
589
675
|
/** Ends a span exactly once; the span is always ended even if recording attributes fails. */
|
|
590
676
|
const finisher = (span) => {
|
|
@@ -636,6 +722,8 @@ function createTxTracker(options = {}) {
|
|
|
636
722
|
...input.startTime !== void 0 ? { startTime: input.startTime } : {}
|
|
637
723
|
}, parent);
|
|
638
724
|
const finish = finisher(span);
|
|
725
|
+
const startMs = toEpochMs(input.startTime);
|
|
726
|
+
const recordSend = (endTime, errorType) => txMetrics.sendDuration(secondsSince(startMs, endTime), metricAttributes(input.chainId, errorType === void 0 ? {} : { [ATTR_ERROR_TYPE]: errorType }));
|
|
639
727
|
return {
|
|
640
728
|
context: trace.setSpan(parent, span),
|
|
641
729
|
end: (result, second) => finish("record transaction hash", () => {
|
|
@@ -649,10 +737,11 @@ function createTxTracker(options = {}) {
|
|
|
649
737
|
parent
|
|
650
738
|
});
|
|
651
739
|
span.setAttributes(redact({ [ATTR_BLOCKCHAIN_TX_HASH]: hash }));
|
|
740
|
+
recordSend(handleOptions(second).endTime);
|
|
652
741
|
}, handleOptions(second).endTime),
|
|
653
742
|
fail: (error, second, third) => {
|
|
654
743
|
const options = handleOptions(second, third);
|
|
655
|
-
finish("record send failure", () => markError(span, reportedErrorType(error, options), error, errorType(error)), options.endTime);
|
|
744
|
+
finish("record send failure", () => recordSend(options.endTime, markError(span, reportedErrorType(error, options), error, errorType(error))), options.endTime);
|
|
656
745
|
}
|
|
657
746
|
};
|
|
658
747
|
};
|
|
@@ -688,6 +777,8 @@ function createTxTracker(options = {}) {
|
|
|
688
777
|
...explicitStart !== void 0 ? { startTime: explicitStart } : {}
|
|
689
778
|
}, parent);
|
|
690
779
|
const finish = finisher(span);
|
|
780
|
+
const startMs = toEpochMs(explicitStart);
|
|
781
|
+
const recordConfirmation = (endTime, outcome) => txMetrics.confirmationDuration(secondsSince(startMs, endTime), metricAttributes(input.chainId, outcome));
|
|
691
782
|
return {
|
|
692
783
|
active: 0,
|
|
693
784
|
ended: false,
|
|
@@ -697,11 +788,16 @@ function createTxTracker(options = {}) {
|
|
|
697
788
|
links: [{ context: span.spanContext() }, ...sent ? [{ context: sent.spanContext }] : []]
|
|
698
789
|
},
|
|
699
790
|
receipt: (receipt, endTime) => finish("record receipt", () => {
|
|
700
|
-
|
|
791
|
+
const attributes = receiptAttributes(receipt);
|
|
792
|
+
span.setAttributes(redact(attributes));
|
|
701
793
|
if (receipt.status === "reverted") markError(span, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED);
|
|
794
|
+
const status = { [ATTR_BLOCKCHAIN_TX_STATUS]: attributes[ATTR_BLOCKCHAIN_TX_STATUS] };
|
|
795
|
+
recordConfirmation(endTime, status);
|
|
796
|
+
const fee = attributes[ATTR_BLOCKCHAIN_TX_FEE];
|
|
797
|
+
if (typeof fee === "string") txMetrics.fee(BigInt(fee), metricAttributes(input.chainId, status));
|
|
702
798
|
}, endTime),
|
|
703
|
-
timeout: (endTime) => finish("record confirmation timeout", () => markError(span, OBSERVER_TIMEOUT), endTime),
|
|
704
|
-
fail: (error, endTime) => finish("record confirmation failure", () => markError(span, errorType(error), error), endTime),
|
|
799
|
+
timeout: (endTime) => finish("record confirmation timeout", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, OBSERVER_TIMEOUT) }), endTime),
|
|
800
|
+
fail: (error, endTime) => finish("record confirmation failure", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, errorType(error), error) }), endTime),
|
|
705
801
|
replaced: (hash, reason, endTime) => finish("record replacement", () => {
|
|
706
802
|
const attributes = {
|
|
707
803
|
[ATTR_BLOCKCHAIN_TX_STATUS]: BLOCKCHAIN_TX_STATUS_VALUE_REPLACED,
|
|
@@ -709,8 +805,9 @@ function createTxTracker(options = {}) {
|
|
|
709
805
|
};
|
|
710
806
|
if (reason !== void 0 && REPLACEMENT_REASONS.has(reason)) attributes[ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON] = reason;
|
|
711
807
|
span.setAttributes(redact(attributes));
|
|
808
|
+
recordConfirmation(endTime, { [ATTR_BLOCKCHAIN_TX_STATUS]: BLOCKCHAIN_TX_STATUS_VALUE_REPLACED });
|
|
712
809
|
}, endTime),
|
|
713
|
-
unattributable: (endTime) => finish("record unattributable receipt", () => markError(span, ERROR_TYPE_VALUE_OTHER), endTime)
|
|
810
|
+
unattributable: (endTime) => finish("record unattributable receipt", () => recordConfirmation(endTime, { [ATTR_ERROR_TYPE]: markError(span, ERROR_TYPE_VALUE_OTHER) }), endTime)
|
|
714
811
|
};
|
|
715
812
|
};
|
|
716
813
|
/**
|
|
@@ -732,7 +829,7 @@ function createTxTracker(options = {}) {
|
|
|
732
829
|
};
|
|
733
830
|
/**
|
|
734
831
|
* Ends `shared` with `receipt`, attributing it to the transaction that was mined
|
|
735
|
-
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.
|
|
832
|
+
* (https://github.com/selimaytac/hashspan/blob/@hashspan/core@0.7.0/docs/adr/0008-replaced-transactions.md).
|
|
736
833
|
*/
|
|
737
834
|
const endWithReceipt = (chainId, hash, shared, receipt, endTime) => {
|
|
738
835
|
const mined = receipt.transactionHash;
|
|
@@ -868,4 +965,4 @@ function createTxTracker(options = {}) {
|
|
|
868
965
|
};
|
|
869
966
|
}
|
|
870
967
|
//#endregion
|
|
871
|
-
export { ATTR_BLOCKCHAIN_BLOCK_NUMBER, ATTR_BLOCKCHAIN_CHAIN_ID, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR, ATTR_BLOCKCHAIN_OPERATION_NAME, ATTR_BLOCKCHAIN_PAYMENT_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_ASSET, ATTR_BLOCKCHAIN_PAYMENT_PAYER, ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_STATUS, ATTR_BLOCKCHAIN_PAYMENT_VERIFIED, ATTR_BLOCKCHAIN_SYSTEM, ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE, ATTR_BLOCKCHAIN_TX_FEE, ATTR_BLOCKCHAIN_TX_FROM, ATTR_BLOCKCHAIN_TX_GAS_USED, ATTR_BLOCKCHAIN_TX_HASH, ATTR_BLOCKCHAIN_TX_L1_FEE, ATTR_BLOCKCHAIN_TX_NONCE, ATTR_BLOCKCHAIN_TX_REPLACEMENT_HASH, ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON, ATTR_BLOCKCHAIN_TX_REVERT_REASON, ATTR_BLOCKCHAIN_TX_STATUS, ATTR_BLOCKCHAIN_TX_TO, ATTR_BLOCKCHAIN_TX_VALUE, ATTR_ERROR_TYPE, ATTR_GEN_AI_AGENT_ID, ATTR_GEN_AI_AGENT_NAME, ATTR_X402_RESOURCE, ATTR_X402_SCHEME, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402, BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED, BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING, BLOCKCHAIN_PAYMENT_STATUS_VALUE_SETTLED, BLOCKCHAIN_SYSTEM_VALUE_EVM, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_CANCELLED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPLACED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPRICED, BLOCKCHAIN_TX_STATUS_VALUE_REPLACED, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED, BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS, BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT, ERROR_TYPE_VALUE_OTHER, VERSION, createTxTracker };
|
|
968
|
+
export { ATTR_BLOCKCHAIN_BLOCK_NUMBER, ATTR_BLOCKCHAIN_CHAIN_ID, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_ARGUMENTS, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_NAME, ATTR_BLOCKCHAIN_CONTRACT_FUNCTION_SELECTOR, ATTR_BLOCKCHAIN_OPERATION_NAME, ATTR_BLOCKCHAIN_PAYMENT_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_ASSET, ATTR_BLOCKCHAIN_PAYMENT_PAYER, ATTR_BLOCKCHAIN_PAYMENT_PROTOCOL, ATTR_BLOCKCHAIN_PAYMENT_RECIPIENT, ATTR_BLOCKCHAIN_PAYMENT_SETTLED_AMOUNT, ATTR_BLOCKCHAIN_PAYMENT_STATUS, ATTR_BLOCKCHAIN_PAYMENT_VERIFIED, ATTR_BLOCKCHAIN_SYSTEM, ATTR_BLOCKCHAIN_TX_EFFECTIVE_GAS_PRICE, ATTR_BLOCKCHAIN_TX_FEE, ATTR_BLOCKCHAIN_TX_FROM, ATTR_BLOCKCHAIN_TX_GAS_USED, ATTR_BLOCKCHAIN_TX_HASH, ATTR_BLOCKCHAIN_TX_L1_FEE, ATTR_BLOCKCHAIN_TX_NONCE, ATTR_BLOCKCHAIN_TX_REPLACEMENT_HASH, ATTR_BLOCKCHAIN_TX_REPLACEMENT_REASON, ATTR_BLOCKCHAIN_TX_REVERT_REASON, ATTR_BLOCKCHAIN_TX_STATUS, ATTR_BLOCKCHAIN_TX_TO, ATTR_BLOCKCHAIN_TX_VALUE, ATTR_ERROR_TYPE, ATTR_GEN_AI_AGENT_ID, ATTR_GEN_AI_AGENT_NAME, ATTR_X402_RESOURCE, ATTR_X402_SCHEME, BLOCKCHAIN_OPERATION_NAME_VALUE_CONFIRM, BLOCKCHAIN_OPERATION_NAME_VALUE_PAYMENT, BLOCKCHAIN_OPERATION_NAME_VALUE_SEND, BLOCKCHAIN_PAYMENT_PROTOCOL_VALUE_X402, BLOCKCHAIN_PAYMENT_STATUS_VALUE_FAILED, BLOCKCHAIN_PAYMENT_STATUS_VALUE_PENDING, BLOCKCHAIN_PAYMENT_STATUS_VALUE_SETTLED, BLOCKCHAIN_SYSTEM_VALUE_EVM, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_CANCELLED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPLACED, BLOCKCHAIN_TX_REPLACEMENT_REASON_VALUE_REPRICED, BLOCKCHAIN_TX_STATUS_VALUE_REPLACED, BLOCKCHAIN_TX_STATUS_VALUE_REVERTED, BLOCKCHAIN_TX_STATUS_VALUE_SUCCESS, BLOCKCHAIN_TX_STATUS_VALUE_TIMEOUT, ERROR_TYPE_VALUE_OTHER, METRIC_BLOCKCHAIN_CLIENT_CONFIRMATION_DURATION, METRIC_BLOCKCHAIN_CLIENT_FEE, METRIC_BLOCKCHAIN_CLIENT_SEND_DURATION, VERSION, createTxTracker };
|