@thesight/sdk 0.1.1 → 0.2.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 CHANGED
@@ -13,12 +13,24 @@ correlation — consumable by the Sight dashboard or any APM backend
13
13
 
14
14
  ```bash
15
15
  pnpm add @thesight/sdk
16
+ # or npm install @thesight/sdk
17
+ # or yarn add @thesight/sdk
16
18
  ```
17
19
 
18
20
  All OpenTelemetry dependencies are bundled transitively — you do **not**
19
21
  need to `pnpm add @opentelemetry/*` separately.
20
22
 
21
- ## Quickstart
23
+ Ships both ESM and CJS builds. Works from modern TypeScript projects,
24
+ Node ESM, legacy CJS via `require()`, bundlers, and serverless runtimes.
25
+
26
+ ## Quickstart — `InstrumentedConnection` (automatic spans)
27
+
28
+ The simplest pattern: swap your `Connection` for `InstrumentedConnection`
29
+ and every transaction you send through it gets a span automatically —
30
+ no matter whether you're calling it directly, going through an Anchor
31
+ `provider.sendAndConfirm`, using web3.js's top-level
32
+ `sendAndConfirmTransaction`, or letting a wallet adapter drive the
33
+ flow.
22
34
 
23
35
  ```ts
24
36
  import { initSight, InstrumentedConnection } from '@thesight/sdk';
@@ -33,17 +45,73 @@ initSight({
33
45
  // Drop-in replacement for @solana/web3.js Connection
34
46
  const connection = new InstrumentedConnection(clusterApiUrl('mainnet'));
35
47
 
36
- // Every confirmed transaction becomes an OTel span routed to Sight ingest
37
- const { signature, cpiTree } = await connection.sendAndConfirmInstrumented(tx);
48
+ // Anywhere a Solana tx goes out, a span goes with it:
49
+ await connection.sendRawTransaction(tx.serialize());
50
+ // or...
51
+ await sendAndConfirmTransaction(connection, tx, signers);
52
+ // or...
53
+ const program = new Program(idl, programId, new AnchorProvider(connection, wallet, {}));
54
+ await program.methods.foo().rpc();
55
+ // or...
56
+ await wallet.sendTransaction(tx, connection);
57
+ ```
58
+
59
+ Under the hood, `InstrumentedConnection` overrides the single method
60
+ every Solana send-path eventually calls — `sendRawTransaction` — so you
61
+ get spans regardless of which layer is driving the transaction.
62
+
63
+ ### What the span does, step by step
64
+
65
+ 1. On the synchronous submit path, a span named `solana.sendRawTransaction`
66
+ starts as a child of whatever OTel context is active.
67
+ 2. `super.sendRawTransaction(rawTx, options)` is called verbatim — **no
68
+ byte mutation, no interception of signing, no private key access**.
69
+ 3. The returned signature is attached to the span and the caller gets
70
+ it back immediately.
71
+ 4. In the background (never blocking the caller), a task polls
72
+ `getTransaction(signature)` with exponential backoff until the
73
+ on-chain record is available, parses the program logs into a CPI
74
+ tree via `@thesight/core`, enriches with registered Anchor IDLs,
75
+ and adds `cpi.invoke` events, per-program CU attribution, and
76
+ decoded error details to the span.
77
+ 5. The span ends and flushes via the Sight exporter configured by
78
+ `initSight`.
79
+
80
+ ## Alternative — `trackSolanaTransaction` (non-wrapping)
81
+
82
+ If you don't want to wrap your `Connection` at all, `trackSolanaTransaction`
83
+ observes by signature alone after you've already sent the transaction
84
+ through *any* connection:
85
+
86
+ ```ts
87
+ import { Connection } from '@solana/web3.js';
88
+ import { initSight, trackSolanaTransaction } from '@thesight/sdk';
89
+
90
+ initSight({ apiKey, serviceName: 'checkout' });
91
+
92
+ // Use your normal Connection, unchanged.
93
+ const connection = new Connection(rpcUrl);
94
+ const signature = await wallet.sendTransaction(tx, connection);
95
+
96
+ // Fire-and-forget — span flushes on its own in the background.
97
+ void trackSolanaTransaction({
98
+ signature,
99
+ connection,
100
+ serviceName: 'checkout',
101
+ idls: { [swapProgramId]: swapIdl },
102
+ });
38
103
  ```
39
104
 
40
- `initSight` registers a `NodeTracerProvider` as the global OpenTelemetry
41
- tracer, installs a batch span processor, and wires a Sight-specific
42
- exporter that POSTs to ingest in the expected format. Any tracer
43
- obtained via `trace.getTracer(...)` (including the one
44
- `InstrumentedConnection` uses internally) routes through the same
45
- exporter so if you add OTel instrumentation elsewhere in your app,
46
- those spans get Sight-correlated for free.
105
+ This helper **never touches the send path**. It only observes the
106
+ transaction after the fact by calling `getTransaction(signature)`.
107
+ Recommended when:
108
+
109
+ - You're security-conscious and don't want any SDK code on the
110
+ critical transaction path
111
+ - Your send path goes through a wallet adapter or a wrapper SDK you
112
+ don't control
113
+ - You want to instrument a specific high-value transaction rather
114
+ than everything a connection sends
47
115
 
48
116
  ## Anchor IDL registration
49
117
 
@@ -73,7 +141,8 @@ being fabricated.
73
141
  ## Self-host the ingest
74
142
 
75
143
  By default spans export to `https://ingest.thesight.dev/ingest`.
76
- Override via `ingestUrl` to point at a self-hosted Sight ingest:
144
+ Override via `ingestUrl` to point at a self-hosted Sight ingest (or a
145
+ local Docker stack for development):
77
146
 
78
147
  ```ts
79
148
  initSight({
@@ -85,24 +154,45 @@ initSight({
85
154
 
86
155
  ## What's on the span
87
156
 
88
- Every `sendAndConfirmInstrumented` call produces an OTel span with the
157
+ Every instrumented transaction produces an OTel span with the
89
158
  following attributes when they're available:
90
159
 
91
160
  - `solana.tx.signature` — base58 signature
92
- - `solana.tx.status` — `confirmed` / `failed` / `timeout`
161
+ - `solana.tx.status` — `submitted` → `confirmed` / `failed` / `timeout`
93
162
  - `solana.tx.slot`
94
163
  - `solana.tx.cu_used` / `solana.tx.cu_budget` / `solana.tx.cu_utilization`
95
164
  - `solana.tx.program` — root program name (from registered IDL or
96
- curated known-programs list)
165
+ the curated known-programs list)
97
166
  - `solana.tx.instruction` — extracted from `Program log: Instruction: …`
98
167
  - `solana.tx.error` / `solana.tx.error_code` / `solana.tx.error_program`
99
168
  — decoded from on-chain return data via the registered IDL's error
100
169
  table or the hardcoded native-error tables
101
170
  - `solana.tx.fee_lamports`
102
- - `solana.tx.submit_ms` / `solana.tx.confirmation_ms` /
103
- `solana.tx.enrichment_ms` — client-observed latencies
104
- - Per-CPI `cpi.invoke` events on the span: program, instruction, depth,
105
- CU consumed, CU self, percentage
171
+ - `solana.tx.submit_ms` / `solana.tx.enrichment_ms` — client-observed
172
+ latencies
173
+ - Per-CPI `cpi.invoke` events on the span: program, instruction,
174
+ depth, CU consumed, CU self, percentage
175
+
176
+ ## Configuration
177
+
178
+ `InstrumentedConnection` accepts these extra options alongside the
179
+ standard web3.js `ConnectionConfig`:
180
+
181
+ ```ts
182
+ new InstrumentedConnection(rpcUrl, {
183
+ // Standard ConnectionConfig fields work as before
184
+ commitment: 'confirmed',
185
+
186
+ // Sight-specific
187
+ tracer: customTracer,
188
+ skipIdlResolution: false, // skip log parsing + IDL decoding
189
+ idls: { [programId]: idl },
190
+ allowOnChainIdlFetch: false, // off by default — never guess
191
+ enrichmentTimeoutMs: 30_000, // background enrichment wall time
192
+ enrichmentPollIntervalMs: 500, // base poll interval with 1.5x backoff
193
+ disableAutoSpan: false, // escape hatch for tests
194
+ });
195
+ ```
106
196
 
107
197
  ## Shutdown
108
198
 
@@ -118,6 +208,33 @@ process.on('SIGTERM', async () => {
118
208
  });
119
209
  ```
120
210
 
211
+ ## Migration from 0.1.x
212
+
213
+ Version 0.2.0 removes the `sendAndConfirmInstrumented()` method that
214
+ earlier versions required you to call explicitly. Spans now fire
215
+ automatically from every call to `sendRawTransaction()` — including
216
+ those made by Anchor's provider, web3.js's top-level helpers, and
217
+ wallet adapters — so explicit wrapping is no longer necessary.
218
+
219
+ **If your 0.1.x code looked like this:**
220
+
221
+ ```ts
222
+ const { signature, cpiTree } = await connection.sendAndConfirmInstrumented(tx);
223
+ ```
224
+
225
+ **Replace it with either:**
226
+
227
+ ```ts
228
+ // 1. Standard web3.js — span fires automatically
229
+ const signature = await sendAndConfirmTransaction(connection, tx, signers);
230
+
231
+ // 2. Or inspect the span attributes via your OTel processor
232
+ ```
233
+
234
+ The `cpiTree` is no longer returned from a direct call. It lives on the
235
+ span as `cpi.invoke` events, accessible via any OTel processor hooked
236
+ into the same tracer provider, or visible in the Sight dashboard.
237
+
121
238
  ## License
122
239
 
123
240
  MIT — see `LICENSE`.
package/dist/index.cjs CHANGED
@@ -20,16 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- IdlResolver: () => import_core3.IdlResolver,
23
+ IdlResolver: () => import_core4.IdlResolver,
24
24
  InstrumentedConnection: () => InstrumentedConnection,
25
25
  SightSpanExporter: () => SightSpanExporter,
26
- initSight: () => initSight
26
+ initSight: () => initSight,
27
+ trackSolanaTransaction: () => trackSolanaTransaction
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
29
30
  var import_web3 = require("@solana/web3.js");
30
- var import_api = require("@opentelemetry/api");
31
- var import_core2 = require("@thesight/core");
31
+ var import_api2 = require("@opentelemetry/api");
32
32
  var import_core3 = require("@thesight/core");
33
+ var import_core4 = require("@thesight/core");
33
34
 
34
35
  // src/init.ts
35
36
  var import_sdk_trace_node = require("@opentelemetry/sdk-trace-node");
@@ -197,6 +198,98 @@ function initSight(config) {
197
198
  };
198
199
  }
199
200
 
201
+ // src/track.ts
202
+ var import_api = require("@opentelemetry/api");
203
+ var import_core2 = require("@thesight/core");
204
+ async function trackSolanaTransaction(opts) {
205
+ const tracerName = opts.serviceName ?? "@thesight/sdk";
206
+ const tracer = opts.tracer ?? import_api.trace.getTracer(tracerName);
207
+ const span = tracer.startSpan("solana.trackTransaction", {}, import_api.context.active());
208
+ span.setAttribute("solana.tx.signature", opts.signature);
209
+ const start = Date.now();
210
+ const deadline = start + (opts.timeoutMs ?? 3e4);
211
+ const commitment = opts.commitment ?? "confirmed";
212
+ const basePoll = opts.pollIntervalMs ?? 500;
213
+ const resolver = opts.idlResolver ?? buildResolverFromIdls(opts.idls);
214
+ try {
215
+ const txDetails = await pollGetTransaction(opts.connection, opts.signature, commitment, deadline, basePoll);
216
+ if (!txDetails) {
217
+ span.setAttribute("solana.tx.status", "timeout");
218
+ span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
219
+ return;
220
+ }
221
+ span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
222
+ span.setAttribute("solana.tx.slot", txDetails.slot);
223
+ const fee = txDetails.meta?.fee;
224
+ if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
225
+ const cuUsed = txDetails.meta?.computeUnitsConsumed;
226
+ if (cuUsed !== void 0 && cuUsed !== null) {
227
+ span.setAttribute("solana.tx.cu_used", Number(cuUsed));
228
+ span.setAttribute("solana.tx.cu_budget", 2e5);
229
+ span.setAttribute(
230
+ "solana.tx.cu_utilization",
231
+ parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
232
+ );
233
+ }
234
+ const logs = txDetails.meta?.logMessages ?? [];
235
+ if (logs.length > 0) {
236
+ const { cpiTree } = (0, import_core2.parseLogs)({ logs });
237
+ await (0, import_core2.enrichTree)(cpiTree, resolver);
238
+ for (const attr of (0, import_core2.flatAttributions)(cpiTree)) {
239
+ span.addEvent("cpi.invoke", {
240
+ "cpi.program": attr.programName ?? attr.programId,
241
+ "cpi.instruction": attr.instructionName ?? "unknown",
242
+ "cpi.depth": attr.depth,
243
+ "cpi.cu_consumed": attr.cuConsumed,
244
+ "cpi.cu_self": attr.cuSelf,
245
+ "cpi.percentage": parseFloat(attr.percentage.toFixed(2))
246
+ });
247
+ }
248
+ const root = cpiTree.roots[0];
249
+ if (root) {
250
+ if (root.programName) span.setAttribute("solana.tx.program", root.programName);
251
+ if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
252
+ }
253
+ }
254
+ span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
255
+ if (txDetails.meta?.err) {
256
+ span.setStatus({ code: import_api.SpanStatusCode.ERROR });
257
+ } else {
258
+ span.setStatus({ code: import_api.SpanStatusCode.OK });
259
+ }
260
+ } catch (err) {
261
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
262
+ span.setStatus({
263
+ code: import_api.SpanStatusCode.ERROR,
264
+ message: err instanceof Error ? err.message : String(err)
265
+ });
266
+ } finally {
267
+ span.end();
268
+ }
269
+ }
270
+ function buildResolverFromIdls(idls) {
271
+ const resolver = new import_core2.IdlResolver();
272
+ if (idls) resolver.registerMany(idls);
273
+ return resolver;
274
+ }
275
+ async function pollGetTransaction(connection, signature, commitment, deadline, basePollMs) {
276
+ let attempt = 0;
277
+ while (Date.now() < deadline) {
278
+ try {
279
+ const tx = await connection.getTransaction(signature, {
280
+ commitment,
281
+ maxSupportedTransactionVersion: 0
282
+ });
283
+ if (tx) return tx;
284
+ } catch {
285
+ }
286
+ attempt++;
287
+ const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
288
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
289
+ }
290
+ return null;
291
+ }
292
+
200
293
  // src/index.ts
201
294
  var InstrumentedConnection = class extends import_web3.Connection {
202
295
  sightConfig;
@@ -209,16 +302,29 @@ var InstrumentedConnection = class extends import_web3.Connection {
209
302
  skipIdlResolution,
210
303
  idls,
211
304
  allowOnChainIdlFetch,
305
+ enrichmentTimeoutMs,
306
+ enrichmentPollIntervalMs,
307
+ disableAutoSpan,
308
+ commitment,
212
309
  ...connectionConfig
213
310
  } = config ?? {};
214
311
  super(endpoint, connectionConfig);
215
- this.sightConfig = { tracer, idlRpcEndpoint, skipIdlResolution, allowOnChainIdlFetch };
216
- this.idlResolver = new import_core2.IdlResolver({
312
+ this.sightConfig = {
313
+ tracer,
314
+ idlRpcEndpoint,
315
+ skipIdlResolution,
316
+ allowOnChainIdlFetch,
317
+ enrichmentTimeoutMs: enrichmentTimeoutMs ?? 3e4,
318
+ enrichmentPollIntervalMs: enrichmentPollIntervalMs ?? 500,
319
+ disableAutoSpan,
320
+ commitment
321
+ };
322
+ this.idlResolver = new import_core3.IdlResolver({
217
323
  rpcEndpoint: idlRpcEndpoint ?? endpoint,
218
324
  allowOnChainFetch: allowOnChainIdlFetch ?? false
219
325
  });
220
326
  if (idls) this.idlResolver.registerMany(idls);
221
- this.tracer = tracer ?? import_api.trace.getTracer("@thesight/sdk");
327
+ this.tracer = tracer ?? import_api2.trace.getTracer("@thesight/sdk");
222
328
  }
223
329
  /**
224
330
  * Register an IDL for a single program. Convenience forwarder for the
@@ -235,132 +341,165 @@ var InstrumentedConnection = class extends import_web3.Connection {
235
341
  registerIdls(idls) {
236
342
  this.idlResolver.registerMany(idls);
237
343
  }
344
+ // ─── Overridden method ────────────────────────────────────────────────────
238
345
  /**
239
- * Sends and confirms a transaction, wrapped in an OTel span.
240
- * The span is a child of the current active context.
346
+ * Overrides the parent `Connection.sendRawTransaction` so every submit
347
+ * including those made by Anchor's `provider.sendAndConfirm`, web3.js's
348
+ * top-level `sendAndConfirmTransaction`, and wallet adapter flows — is
349
+ * observed by an OTel span automatically.
350
+ *
351
+ * The span is a child of whatever OTel context is active when the call
352
+ * fires, so app-level parent spans (HTTP handlers, job runs, wallet flow
353
+ * spans from another SDK) nest the Sight span correctly.
241
354
  */
242
- async sendAndConfirmInstrumented(transaction, options) {
243
- const commitment = options?.commitment ?? this.sightConfig.commitment ?? "confirmed";
244
- const finality = commitment === "processed" ? "confirmed" : commitment;
245
- const span = this.tracer.startSpan("solana.sendAndConfirmTransaction", {}, import_api.context.active());
355
+ async sendRawTransaction(rawTransaction, options) {
356
+ if (this.sightConfig.disableAutoSpan) {
357
+ return super.sendRawTransaction(rawTransaction, options);
358
+ }
359
+ const span = this.tracer.startSpan("solana.sendRawTransaction", {}, import_api2.context.active());
246
360
  const submitStart = Date.now();
361
+ let signature;
247
362
  try {
248
- const signature = await this.sendRawTransaction(
249
- "serialize" in transaction ? transaction.serialize() : transaction.serialize(),
250
- { skipPreflight: false, ...options }
251
- );
252
- span.setAttribute("solana.tx.signature", signature);
363
+ signature = await super.sendRawTransaction(rawTransaction, options);
364
+ } catch (err) {
253
365
  const submitMs = Date.now() - submitStart;
254
366
  span.setAttribute("solana.tx.submit_ms", submitMs);
255
- const confirmStart = Date.now();
256
- const { value: result } = await this.confirmTransaction(
257
- { signature, ...await this.getLatestBlockhash(commitment) },
258
- commitment
259
- );
260
- const confirmMs = Date.now() - confirmStart;
261
- span.setAttribute("solana.tx.confirmation_ms", confirmMs);
262
- if (result.err) {
263
- span.setAttribute("solana.tx.status", "failed");
264
- span.setStatus({ code: import_api.SpanStatusCode.ERROR });
265
- if (!this.sightConfig.skipIdlResolution) {
266
- const txDetails = await this.getParsedTransaction(signature, {
267
- commitment: finality,
268
- maxSupportedTransactionVersion: 0
269
- });
270
- const accountKeys = txDetails?.transaction.message.accountKeys.map(
271
- (k) => k.pubkey ? k.pubkey.toBase58() : k.toBase58()
272
- ) ?? [];
273
- const programIds = txDetails?.transaction.message.instructions.map(
274
- (ix) => ix.programId ? ix.programId.toBase58() : accountKeys[ix.programIdIndex] ?? ""
275
- ) ?? [];
276
- const decoded = await this.idlResolver.decodeError(
277
- result.err,
278
- accountKeys,
279
- programIds
280
- );
281
- if (decoded.errorName) {
282
- span.setAttribute("solana.tx.error", decoded.errorName);
283
- }
284
- if (decoded.errorCode !== void 0) {
285
- span.setAttribute("solana.tx.error_code", decoded.errorCode);
286
- }
287
- if (decoded.programName) {
288
- span.setAttribute("solana.tx.error_program", decoded.programName);
289
- }
290
- if (decoded.errorMsg) {
291
- span.setAttribute("solana.tx.error_msg", decoded.errorMsg);
292
- }
293
- span.end();
294
- return { signature, error: decoded };
295
- }
367
+ span.setAttribute("solana.tx.status", "failed");
368
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
369
+ span.setStatus({
370
+ code: import_api2.SpanStatusCode.ERROR,
371
+ message: err instanceof Error ? err.message : String(err)
372
+ });
373
+ span.end();
374
+ throw err;
375
+ }
376
+ span.setAttribute("solana.tx.signature", signature);
377
+ span.setAttribute("solana.tx.submit_ms", Date.now() - submitStart);
378
+ span.setAttribute("solana.tx.status", "submitted");
379
+ void this.enrichSpanInBackground(span, signature, submitStart);
380
+ return signature;
381
+ }
382
+ // ─── Background enrichment ───────────────────────────────────────────────
383
+ /**
384
+ * Poll `getTransaction` until the on-chain record is available, then
385
+ * enrich the span with CU attribution, program names, decoded errors,
386
+ * and per-CPI events.
387
+ *
388
+ * This runs async and is never awaited by callers of `sendRawTransaction`.
389
+ * Failures inside this method attach to the span via recordException
390
+ * rather than throwing.
391
+ */
392
+ async enrichSpanInBackground(span, signature, submitStart) {
393
+ const enrichStart = Date.now();
394
+ const deadline = enrichStart + (this.sightConfig.enrichmentTimeoutMs ?? 3e4);
395
+ const commitment = this.sightConfig.commitment ?? "confirmed";
396
+ const basePollMs = this.sightConfig.enrichmentPollIntervalMs ?? 500;
397
+ try {
398
+ const txDetails = await this.pollForTransaction(signature, commitment, deadline, basePollMs);
399
+ if (!txDetails) {
400
+ span.setAttribute("solana.tx.status", "timeout");
401
+ span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
296
402
  span.end();
297
- return { signature };
403
+ return;
298
404
  }
299
- span.setAttribute("solana.tx.status", "confirmed");
405
+ this.attachTxDetailsToSpan(span, txDetails);
300
406
  if (!this.sightConfig.skipIdlResolution) {
301
- const enrichStart = Date.now();
302
- const txDetails = await this.getTransaction(signature, {
303
- commitment: finality,
304
- maxSupportedTransactionVersion: 0
305
- });
306
- if (txDetails) {
307
- const slot = txDetails.slot;
308
- const fee = txDetails.meta?.fee;
309
- const logs = txDetails.meta?.logMessages ?? [];
310
- const cuUsed = txDetails.meta?.computeUnitsConsumed;
311
- span.setAttribute("solana.tx.slot", slot);
312
- if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
313
- if (cuUsed !== void 0) {
314
- span.setAttribute("solana.tx.cu_used", cuUsed);
315
- span.setAttribute("solana.tx.cu_budget", 2e5);
316
- span.setAttribute("solana.tx.cu_utilization", parseFloat((cuUsed / 2e5 * 100).toFixed(1)));
317
- }
318
- const { cpiTree } = (0, import_core2.parseLogs)({ logs });
319
- if (!this.sightConfig.skipIdlResolution) {
320
- await (0, import_core2.enrichTree)(cpiTree, this.idlResolver);
321
- }
322
- const attributions = (0, import_core2.flatAttributions)(cpiTree);
323
- for (const attr of attributions) {
324
- span.addEvent("cpi.invoke", {
325
- "cpi.program": attr.programName ?? attr.programId,
326
- "cpi.instruction": attr.instructionName ?? "unknown",
327
- "cpi.depth": attr.depth,
328
- "cpi.cu_consumed": attr.cuConsumed,
329
- "cpi.cu_self": attr.cuSelf,
330
- "cpi.percentage": parseFloat(attr.percentage.toFixed(2))
331
- });
332
- }
333
- const root = cpiTree.roots[0];
334
- if (root) {
335
- if (root.programName) span.setAttribute("solana.tx.program", root.programName);
336
- if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
337
- }
338
- span.setAttribute("solana.tx.enrichment_ms", Date.now() - enrichStart);
339
- span.setStatus({ code: import_api.SpanStatusCode.OK });
340
- span.end();
341
- return { signature, cpiTree };
407
+ const logs = txDetails.meta?.logMessages ?? [];
408
+ if (logs.length > 0) {
409
+ await this.attachParsedLogsToSpan(span, logs);
342
410
  }
343
411
  }
344
- span.setStatus({ code: import_api.SpanStatusCode.OK });
345
- span.end();
346
- return { signature };
412
+ span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
413
+ if (txDetails.meta?.err) {
414
+ span.setStatus({ code: import_api2.SpanStatusCode.ERROR });
415
+ } else {
416
+ span.setStatus({ code: import_api2.SpanStatusCode.OK });
417
+ }
347
418
  } catch (err) {
348
- span.setAttribute("solana.tx.status", "failed");
349
- span.setAttribute("solana.tx.submit_ms", Date.now() - submitStart);
350
- span.setStatus({
351
- code: import_api.SpanStatusCode.ERROR,
352
- message: err instanceof Error ? err.message : String(err)
353
- });
419
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
420
+ span.setAttribute(
421
+ "solana.tx.enrichment_error",
422
+ err instanceof Error ? err.message : String(err)
423
+ );
424
+ } finally {
354
425
  span.end();
355
- throw err;
356
426
  }
357
427
  }
428
+ /**
429
+ * Poll `getTransaction(signature)` until either the on-chain record is
430
+ * returned or the deadline passes. Exponential backoff (1.5x) capped at
431
+ * 2 seconds to balance responsiveness against RPC load.
432
+ */
433
+ async pollForTransaction(signature, commitment, deadline, basePollMs) {
434
+ let attempt = 0;
435
+ while (Date.now() < deadline) {
436
+ try {
437
+ const tx = await super.getTransaction(signature, {
438
+ commitment,
439
+ maxSupportedTransactionVersion: 0
440
+ });
441
+ if (tx) return tx;
442
+ } catch {
443
+ }
444
+ attempt++;
445
+ const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
446
+ await sleep(waitMs);
447
+ }
448
+ return null;
449
+ }
450
+ /** Attach the flat fields (slot, fee, CU, status) from a tx response to a span. */
451
+ attachTxDetailsToSpan(span, txDetails) {
452
+ span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
453
+ span.setAttribute("solana.tx.slot", txDetails.slot);
454
+ const fee = txDetails.meta?.fee;
455
+ if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
456
+ const cuUsed = txDetails.meta?.computeUnitsConsumed;
457
+ if (cuUsed !== void 0 && cuUsed !== null) {
458
+ span.setAttribute("solana.tx.cu_used", Number(cuUsed));
459
+ span.setAttribute("solana.tx.cu_budget", 2e5);
460
+ span.setAttribute(
461
+ "solana.tx.cu_utilization",
462
+ parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
463
+ );
464
+ }
465
+ }
466
+ /**
467
+ * Parse program logs into a CPI tree, enrich with registered IDLs, and
468
+ * emit one `cpi.invoke` event per invocation onto the span. Root
469
+ * program/instruction names are copied onto span attributes so dashboards
470
+ * can filter by them without walking events.
471
+ */
472
+ async attachParsedLogsToSpan(span, logs) {
473
+ const { cpiTree } = (0, import_core3.parseLogs)({ logs });
474
+ await (0, import_core3.enrichTree)(cpiTree, this.idlResolver);
475
+ const attributions = (0, import_core3.flatAttributions)(cpiTree);
476
+ for (const attr of attributions) {
477
+ span.addEvent("cpi.invoke", {
478
+ "cpi.program": attr.programName ?? attr.programId,
479
+ "cpi.instruction": attr.instructionName ?? "unknown",
480
+ "cpi.depth": attr.depth,
481
+ "cpi.cu_consumed": attr.cuConsumed,
482
+ "cpi.cu_self": attr.cuSelf,
483
+ "cpi.percentage": parseFloat(attr.percentage.toFixed(2))
484
+ });
485
+ }
486
+ const root = cpiTree.roots[0];
487
+ if (root) {
488
+ if (root.programName) span.setAttribute("solana.tx.program", root.programName);
489
+ if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
490
+ }
491
+ return cpiTree;
492
+ }
358
493
  };
494
+ function sleep(ms) {
495
+ return new Promise((resolve) => setTimeout(resolve, ms));
496
+ }
359
497
  // Annotate the CommonJS export names for ESM import in node:
360
498
  0 && (module.exports = {
361
499
  IdlResolver,
362
500
  InstrumentedConnection,
363
501
  SightSpanExporter,
364
- initSight
502
+ initSight,
503
+ trackSolanaTransaction
365
504
  });
366
505
  //# sourceMappingURL=index.cjs.map