@thesight/sdk 0.1.0 → 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 +135 -18
- package/dist/index.cjs +505 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +338 -0
- package/dist/index.d.ts +283 -37
- package/dist/index.js +480 -170
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/dist/exporter.d.ts +0 -43
- package/dist/exporter.d.ts.map +0 -1
- package/dist/exporter.js +0 -151
- package/dist/exporter.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/init.d.ts +0 -62
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -78
- package/dist/init.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,175 +1,485 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
Connection
|
|
4
|
+
} from "@solana/web3.js";
|
|
5
|
+
import {
|
|
6
|
+
trace as trace2,
|
|
7
|
+
context as context2,
|
|
8
|
+
SpanStatusCode as SpanStatusCode2
|
|
9
|
+
} from "@opentelemetry/api";
|
|
10
|
+
import { parseLogs as parseLogs2, IdlResolver as IdlResolver2, enrichTree as enrichTree2, flatAttributions as flatAttributions2 } from "@thesight/core";
|
|
11
|
+
import { IdlResolver as IdlResolver3 } from "@thesight/core";
|
|
12
|
+
|
|
13
|
+
// src/init.ts
|
|
14
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
15
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
16
|
+
import { Resource } from "@opentelemetry/resources";
|
|
17
|
+
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
18
|
+
|
|
19
|
+
// src/exporter.ts
|
|
20
|
+
import {
|
|
21
|
+
hrTimeToMilliseconds,
|
|
22
|
+
ExportResultCode
|
|
23
|
+
} from "@opentelemetry/core";
|
|
24
|
+
var SightSpanExporter = class {
|
|
25
|
+
apiKey;
|
|
26
|
+
ingestUrl;
|
|
27
|
+
fetchImpl;
|
|
28
|
+
maxBatchSize;
|
|
29
|
+
shuttingDown = false;
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this.apiKey = config.apiKey;
|
|
32
|
+
this.ingestUrl = config.ingestUrl;
|
|
33
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
34
|
+
this.maxBatchSize = Math.min(100, config.maxBatchSize ?? 100);
|
|
35
|
+
if (!this.fetchImpl) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"SightSpanExporter: globalThis.fetch is not available. Node >= 18 ships with fetch built in, or pass `fetchImpl` explicitly."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async export(spans, resultCallback) {
|
|
42
|
+
if (this.shuttingDown) {
|
|
43
|
+
resultCallback({ code: ExportResultCode.FAILED, error: new Error("Exporter is shut down") });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const chunks = [];
|
|
47
|
+
for (let i = 0; i < spans.length; i += this.maxBatchSize) {
|
|
48
|
+
chunks.push(spans.slice(i, i + this.maxBatchSize));
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
for (const chunk of chunks) {
|
|
52
|
+
await this.sendChunk(chunk);
|
|
53
|
+
}
|
|
54
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
resultCallback({
|
|
57
|
+
code: ExportResultCode.FAILED,
|
|
58
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async shutdown() {
|
|
63
|
+
this.shuttingDown = true;
|
|
64
|
+
}
|
|
65
|
+
async forceFlush() {
|
|
66
|
+
}
|
|
67
|
+
// ─── Internal ──────────────────────────────────────────────────────────────
|
|
68
|
+
async sendChunk(spans) {
|
|
69
|
+
const payload = {
|
|
70
|
+
spans: spans.map((span) => this.convertSpan(span))
|
|
71
|
+
};
|
|
72
|
+
const response = await this.fetchImpl(this.ingestUrl, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(payload)
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const body = await safeReadText(response);
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Sight ingest returned ${response.status} ${response.statusText}: ${body}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
convertSpan(span) {
|
|
88
|
+
const attr = span.attributes;
|
|
89
|
+
const resource = span.resource.attributes;
|
|
90
|
+
const serviceName = typeof resource["service.name"] === "string" ? resource["service.name"] : void 0;
|
|
91
|
+
const startTimeMs = hrTimeToMilliseconds(span.startTime);
|
|
92
|
+
const endTimeMs = hrTimeToMilliseconds(span.endTime);
|
|
93
|
+
const durationMs = Math.max(0, endTimeMs - startTimeMs);
|
|
94
|
+
const out = {
|
|
95
|
+
traceId: span.spanContext().traceId,
|
|
96
|
+
spanId: span.spanContext().spanId,
|
|
97
|
+
spanName: span.name,
|
|
98
|
+
startTimeMs,
|
|
99
|
+
durationMs
|
|
100
|
+
};
|
|
101
|
+
const parentSpanId = span.parentSpanId;
|
|
102
|
+
if (parentSpanId) out.parentSpanId = parentSpanId;
|
|
103
|
+
if (serviceName) out.serviceName = serviceName;
|
|
104
|
+
copyIfString(attr, "solana.tx.signature", out);
|
|
105
|
+
copyIfEnum(attr, "solana.tx.status", out, ["confirmed", "failed", "timeout"]);
|
|
106
|
+
copyIfNumber(attr, "solana.tx.slot", out);
|
|
107
|
+
copyIfNumber(attr, "solana.tx.cu_used", out);
|
|
108
|
+
copyIfNumber(attr, "solana.tx.cu_budget", out);
|
|
109
|
+
copyIfNumber(attr, "solana.tx.cu_utilization", out);
|
|
110
|
+
copyIfString(attr, "solana.tx.program", out);
|
|
111
|
+
copyIfString(attr, "solana.tx.instruction", out);
|
|
112
|
+
copyIfString(attr, "solana.tx.error", out);
|
|
113
|
+
copyIfNumber(attr, "solana.tx.error_code", out);
|
|
114
|
+
copyIfString(attr, "solana.tx.error_program", out);
|
|
115
|
+
copyIfNumber(attr, "solana.tx.submit_ms", out);
|
|
116
|
+
copyIfNumber(attr, "solana.tx.confirmation_ms", out);
|
|
117
|
+
copyIfNumber(attr, "solana.tx.fee_lamports", out);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function copyIfString(attr, key, out) {
|
|
122
|
+
const v = attr[key];
|
|
123
|
+
if (typeof v === "string") {
|
|
124
|
+
out[key] = v;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function copyIfNumber(attr, key, out) {
|
|
128
|
+
const v = attr[key];
|
|
129
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
130
|
+
out[key] = v;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function copyIfEnum(attr, key, out, allowed) {
|
|
134
|
+
const v = attr[key];
|
|
135
|
+
if (typeof v === "string" && allowed.includes(v)) {
|
|
136
|
+
out[key] = v;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function safeReadText(res) {
|
|
140
|
+
try {
|
|
141
|
+
return (await res.text()).slice(0, 500);
|
|
142
|
+
} catch {
|
|
143
|
+
return "<no body>";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/init.ts
|
|
148
|
+
var DEFAULT_INGEST_URL = "https://ingest.thesight.dev/ingest";
|
|
149
|
+
function initSight(config) {
|
|
150
|
+
if (!config.apiKey) {
|
|
151
|
+
throw new Error("initSight: `apiKey` is required");
|
|
152
|
+
}
|
|
153
|
+
if (!config.serviceName) {
|
|
154
|
+
throw new Error("initSight: `serviceName` is required");
|
|
155
|
+
}
|
|
156
|
+
const resource = new Resource({
|
|
157
|
+
[SEMRESATTRS_SERVICE_NAME]: config.serviceName,
|
|
158
|
+
...config.serviceVersion ? { [SEMRESATTRS_SERVICE_VERSION]: config.serviceVersion } : {}
|
|
159
|
+
});
|
|
160
|
+
const provider = new NodeTracerProvider({ resource });
|
|
161
|
+
const exporter = new SightSpanExporter({
|
|
162
|
+
apiKey: config.apiKey,
|
|
163
|
+
ingestUrl: config.ingestUrl ?? DEFAULT_INGEST_URL,
|
|
164
|
+
fetchImpl: config.fetchImpl,
|
|
165
|
+
maxBatchSize: config.maxBatchSize
|
|
166
|
+
});
|
|
167
|
+
const processor = new BatchSpanProcessor(exporter, {
|
|
168
|
+
scheduledDelayMillis: config.batchDelayMs ?? 5e3,
|
|
169
|
+
maxExportBatchSize: config.maxBatchSize ?? 100,
|
|
170
|
+
maxQueueSize: 2048
|
|
171
|
+
});
|
|
172
|
+
provider.addSpanProcessor(processor);
|
|
173
|
+
provider.register();
|
|
174
|
+
return {
|
|
175
|
+
provider,
|
|
176
|
+
shutdown: async () => {
|
|
177
|
+
await provider.shutdown();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/track.ts
|
|
183
|
+
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
|
|
184
|
+
import { parseLogs, IdlResolver, enrichTree, flatAttributions } from "@thesight/core";
|
|
185
|
+
async function trackSolanaTransaction(opts) {
|
|
186
|
+
const tracerName = opts.serviceName ?? "@thesight/sdk";
|
|
187
|
+
const tracer = opts.tracer ?? trace.getTracer(tracerName);
|
|
188
|
+
const span = tracer.startSpan("solana.trackTransaction", {}, context.active());
|
|
189
|
+
span.setAttribute("solana.tx.signature", opts.signature);
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
const deadline = start + (opts.timeoutMs ?? 3e4);
|
|
192
|
+
const commitment = opts.commitment ?? "confirmed";
|
|
193
|
+
const basePoll = opts.pollIntervalMs ?? 500;
|
|
194
|
+
const resolver = opts.idlResolver ?? buildResolverFromIdls(opts.idls);
|
|
195
|
+
try {
|
|
196
|
+
const txDetails = await pollGetTransaction(opts.connection, opts.signature, commitment, deadline, basePoll);
|
|
197
|
+
if (!txDetails) {
|
|
198
|
+
span.setAttribute("solana.tx.status", "timeout");
|
|
199
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
|
|
203
|
+
span.setAttribute("solana.tx.slot", txDetails.slot);
|
|
204
|
+
const fee = txDetails.meta?.fee;
|
|
205
|
+
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
206
|
+
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
207
|
+
if (cuUsed !== void 0 && cuUsed !== null) {
|
|
208
|
+
span.setAttribute("solana.tx.cu_used", Number(cuUsed));
|
|
209
|
+
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
210
|
+
span.setAttribute(
|
|
211
|
+
"solana.tx.cu_utilization",
|
|
212
|
+
parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const logs = txDetails.meta?.logMessages ?? [];
|
|
216
|
+
if (logs.length > 0) {
|
|
217
|
+
const { cpiTree } = parseLogs({ logs });
|
|
218
|
+
await enrichTree(cpiTree, resolver);
|
|
219
|
+
for (const attr of flatAttributions(cpiTree)) {
|
|
220
|
+
span.addEvent("cpi.invoke", {
|
|
221
|
+
"cpi.program": attr.programName ?? attr.programId,
|
|
222
|
+
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
223
|
+
"cpi.depth": attr.depth,
|
|
224
|
+
"cpi.cu_consumed": attr.cuConsumed,
|
|
225
|
+
"cpi.cu_self": attr.cuSelf,
|
|
226
|
+
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
28
227
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
}
|
|
229
|
+
const root = cpiTree.roots[0];
|
|
230
|
+
if (root) {
|
|
231
|
+
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
232
|
+
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - start);
|
|
236
|
+
if (txDetails.meta?.err) {
|
|
237
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
238
|
+
} else {
|
|
239
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
243
|
+
span.setStatus({
|
|
244
|
+
code: SpanStatusCode.ERROR,
|
|
245
|
+
message: err instanceof Error ? err.message : String(err)
|
|
246
|
+
});
|
|
247
|
+
} finally {
|
|
248
|
+
span.end();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function buildResolverFromIdls(idls) {
|
|
252
|
+
const resolver = new IdlResolver();
|
|
253
|
+
if (idls) resolver.registerMany(idls);
|
|
254
|
+
return resolver;
|
|
255
|
+
}
|
|
256
|
+
async function pollGetTransaction(connection, signature, commitment, deadline, basePollMs) {
|
|
257
|
+
let attempt = 0;
|
|
258
|
+
while (Date.now() < deadline) {
|
|
259
|
+
try {
|
|
260
|
+
const tx = await connection.getTransaction(signature, {
|
|
261
|
+
commitment,
|
|
262
|
+
maxSupportedTransactionVersion: 0
|
|
263
|
+
});
|
|
264
|
+
if (tx) return tx;
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
attempt++;
|
|
268
|
+
const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/index.ts
|
|
275
|
+
var InstrumentedConnection = class extends Connection {
|
|
276
|
+
sightConfig;
|
|
277
|
+
idlResolver;
|
|
278
|
+
tracer;
|
|
279
|
+
constructor(endpoint, config) {
|
|
280
|
+
const {
|
|
281
|
+
tracer,
|
|
282
|
+
idlRpcEndpoint,
|
|
283
|
+
skipIdlResolution,
|
|
284
|
+
idls,
|
|
285
|
+
allowOnChainIdlFetch,
|
|
286
|
+
enrichmentTimeoutMs,
|
|
287
|
+
enrichmentPollIntervalMs,
|
|
288
|
+
disableAutoSpan,
|
|
289
|
+
commitment,
|
|
290
|
+
...connectionConfig
|
|
291
|
+
} = config ?? {};
|
|
292
|
+
super(endpoint, connectionConfig);
|
|
293
|
+
this.sightConfig = {
|
|
294
|
+
tracer,
|
|
295
|
+
idlRpcEndpoint,
|
|
296
|
+
skipIdlResolution,
|
|
297
|
+
allowOnChainIdlFetch,
|
|
298
|
+
enrichmentTimeoutMs: enrichmentTimeoutMs ?? 3e4,
|
|
299
|
+
enrichmentPollIntervalMs: enrichmentPollIntervalMs ?? 500,
|
|
300
|
+
disableAutoSpan,
|
|
301
|
+
commitment
|
|
302
|
+
};
|
|
303
|
+
this.idlResolver = new IdlResolver2({
|
|
304
|
+
rpcEndpoint: idlRpcEndpoint ?? endpoint,
|
|
305
|
+
allowOnChainFetch: allowOnChainIdlFetch ?? false
|
|
306
|
+
});
|
|
307
|
+
if (idls) this.idlResolver.registerMany(idls);
|
|
308
|
+
this.tracer = tracer ?? trace2.getTracer("@thesight/sdk");
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Register an IDL for a single program. Convenience forwarder for the
|
|
312
|
+
* underlying resolver. Anchor users typically call this right after
|
|
313
|
+
* constructing a `Program`:
|
|
314
|
+
*
|
|
315
|
+
* const program = new Program(idl, programId, provider);
|
|
316
|
+
* connection.registerIdl(program.programId.toBase58(), program.idl);
|
|
317
|
+
*/
|
|
318
|
+
registerIdl(programId, idl) {
|
|
319
|
+
this.idlResolver.register(programId, idl);
|
|
320
|
+
}
|
|
321
|
+
/** Bulk-register multiple IDLs. */
|
|
322
|
+
registerIdls(idls) {
|
|
323
|
+
this.idlResolver.registerMany(idls);
|
|
324
|
+
}
|
|
325
|
+
// ─── Overridden method ────────────────────────────────────────────────────
|
|
326
|
+
/**
|
|
327
|
+
* Overrides the parent `Connection.sendRawTransaction` so every submit —
|
|
328
|
+
* including those made by Anchor's `provider.sendAndConfirm`, web3.js's
|
|
329
|
+
* top-level `sendAndConfirmTransaction`, and wallet adapter flows — is
|
|
330
|
+
* observed by an OTel span automatically.
|
|
331
|
+
*
|
|
332
|
+
* The span is a child of whatever OTel context is active when the call
|
|
333
|
+
* fires, so app-level parent spans (HTTP handlers, job runs, wallet flow
|
|
334
|
+
* spans from another SDK) nest the Sight span correctly.
|
|
335
|
+
*/
|
|
336
|
+
async sendRawTransaction(rawTransaction, options) {
|
|
337
|
+
if (this.sightConfig.disableAutoSpan) {
|
|
338
|
+
return super.sendRawTransaction(rawTransaction, options);
|
|
339
|
+
}
|
|
340
|
+
const span = this.tracer.startSpan("solana.sendRawTransaction", {}, context2.active());
|
|
341
|
+
const submitStart = Date.now();
|
|
342
|
+
let signature;
|
|
343
|
+
try {
|
|
344
|
+
signature = await super.sendRawTransaction(rawTransaction, options);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const submitMs = Date.now() - submitStart;
|
|
347
|
+
span.setAttribute("solana.tx.submit_ms", submitMs);
|
|
348
|
+
span.setAttribute("solana.tx.status", "failed");
|
|
349
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
350
|
+
span.setStatus({
|
|
351
|
+
code: SpanStatusCode2.ERROR,
|
|
352
|
+
message: err instanceof Error ? err.message : String(err)
|
|
353
|
+
});
|
|
354
|
+
span.end();
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
span.setAttribute("solana.tx.signature", signature);
|
|
358
|
+
span.setAttribute("solana.tx.submit_ms", Date.now() - submitStart);
|
|
359
|
+
span.setAttribute("solana.tx.status", "submitted");
|
|
360
|
+
void this.enrichSpanInBackground(span, signature, submitStart);
|
|
361
|
+
return signature;
|
|
362
|
+
}
|
|
363
|
+
// ─── Background enrichment ───────────────────────────────────────────────
|
|
364
|
+
/**
|
|
365
|
+
* Poll `getTransaction` until the on-chain record is available, then
|
|
366
|
+
* enrich the span with CU attribution, program names, decoded errors,
|
|
367
|
+
* and per-CPI events.
|
|
368
|
+
*
|
|
369
|
+
* This runs async and is never awaited by callers of `sendRawTransaction`.
|
|
370
|
+
* Failures inside this method attach to the span via recordException
|
|
371
|
+
* rather than throwing.
|
|
372
|
+
*/
|
|
373
|
+
async enrichSpanInBackground(span, signature, submitStart) {
|
|
374
|
+
const enrichStart = Date.now();
|
|
375
|
+
const deadline = enrichStart + (this.sightConfig.enrichmentTimeoutMs ?? 3e4);
|
|
376
|
+
const commitment = this.sightConfig.commitment ?? "confirmed";
|
|
377
|
+
const basePollMs = this.sightConfig.enrichmentPollIntervalMs ?? 500;
|
|
378
|
+
try {
|
|
379
|
+
const txDetails = await this.pollForTransaction(signature, commitment, deadline, basePollMs);
|
|
380
|
+
if (!txDetails) {
|
|
381
|
+
span.setAttribute("solana.tx.status", "timeout");
|
|
382
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
|
|
383
|
+
span.end();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this.attachTxDetailsToSpan(span, txDetails);
|
|
387
|
+
if (!this.sightConfig.skipIdlResolution) {
|
|
388
|
+
const logs = txDetails.meta?.logMessages ?? [];
|
|
389
|
+
if (logs.length > 0) {
|
|
390
|
+
await this.attachParsedLogsToSpan(span, logs);
|
|
165
391
|
}
|
|
392
|
+
}
|
|
393
|
+
span.setAttribute("solana.tx.enrichment_ms", Date.now() - submitStart);
|
|
394
|
+
if (txDetails.meta?.err) {
|
|
395
|
+
span.setStatus({ code: SpanStatusCode2.ERROR });
|
|
396
|
+
} else {
|
|
397
|
+
span.setStatus({ code: SpanStatusCode2.OK });
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
401
|
+
span.setAttribute(
|
|
402
|
+
"solana.tx.enrichment_error",
|
|
403
|
+
err instanceof Error ? err.message : String(err)
|
|
404
|
+
);
|
|
405
|
+
} finally {
|
|
406
|
+
span.end();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Poll `getTransaction(signature)` until either the on-chain record is
|
|
411
|
+
* returned or the deadline passes. Exponential backoff (1.5x) capped at
|
|
412
|
+
* 2 seconds to balance responsiveness against RPC load.
|
|
413
|
+
*/
|
|
414
|
+
async pollForTransaction(signature, commitment, deadline, basePollMs) {
|
|
415
|
+
let attempt = 0;
|
|
416
|
+
while (Date.now() < deadline) {
|
|
417
|
+
try {
|
|
418
|
+
const tx = await super.getTransaction(signature, {
|
|
419
|
+
commitment,
|
|
420
|
+
maxSupportedTransactionVersion: 0
|
|
421
|
+
});
|
|
422
|
+
if (tx) return tx;
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
attempt++;
|
|
426
|
+
const waitMs = Math.min(basePollMs * Math.pow(1.5, attempt - 1), 2e3);
|
|
427
|
+
await sleep(waitMs);
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
/** Attach the flat fields (slot, fee, CU, status) from a tx response to a span. */
|
|
432
|
+
attachTxDetailsToSpan(span, txDetails) {
|
|
433
|
+
span.setAttribute("solana.tx.status", txDetails.meta?.err ? "failed" : "confirmed");
|
|
434
|
+
span.setAttribute("solana.tx.slot", txDetails.slot);
|
|
435
|
+
const fee = txDetails.meta?.fee;
|
|
436
|
+
if (fee !== void 0) span.setAttribute("solana.tx.fee_lamports", fee);
|
|
437
|
+
const cuUsed = txDetails.meta?.computeUnitsConsumed;
|
|
438
|
+
if (cuUsed !== void 0 && cuUsed !== null) {
|
|
439
|
+
span.setAttribute("solana.tx.cu_used", Number(cuUsed));
|
|
440
|
+
span.setAttribute("solana.tx.cu_budget", 2e5);
|
|
441
|
+
span.setAttribute(
|
|
442
|
+
"solana.tx.cu_utilization",
|
|
443
|
+
parseFloat((Number(cuUsed) / 2e5 * 100).toFixed(1))
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Parse program logs into a CPI tree, enrich with registered IDLs, and
|
|
449
|
+
* emit one `cpi.invoke` event per invocation onto the span. Root
|
|
450
|
+
* program/instruction names are copied onto span attributes so dashboards
|
|
451
|
+
* can filter by them without walking events.
|
|
452
|
+
*/
|
|
453
|
+
async attachParsedLogsToSpan(span, logs) {
|
|
454
|
+
const { cpiTree } = parseLogs2({ logs });
|
|
455
|
+
await enrichTree2(cpiTree, this.idlResolver);
|
|
456
|
+
const attributions = flatAttributions2(cpiTree);
|
|
457
|
+
for (const attr of attributions) {
|
|
458
|
+
span.addEvent("cpi.invoke", {
|
|
459
|
+
"cpi.program": attr.programName ?? attr.programId,
|
|
460
|
+
"cpi.instruction": attr.instructionName ?? "unknown",
|
|
461
|
+
"cpi.depth": attr.depth,
|
|
462
|
+
"cpi.cu_consumed": attr.cuConsumed,
|
|
463
|
+
"cpi.cu_self": attr.cuSelf,
|
|
464
|
+
"cpi.percentage": parseFloat(attr.percentage.toFixed(2))
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const root = cpiTree.roots[0];
|
|
468
|
+
if (root) {
|
|
469
|
+
if (root.programName) span.setAttribute("solana.tx.program", root.programName);
|
|
470
|
+
if (root.instructionName) span.setAttribute("solana.tx.instruction", root.instructionName);
|
|
166
471
|
}
|
|
472
|
+
return cpiTree;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
function sleep(ms) {
|
|
476
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
167
477
|
}
|
|
168
|
-
export {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
478
|
+
export {
|
|
479
|
+
IdlResolver3 as IdlResolver,
|
|
480
|
+
InstrumentedConnection,
|
|
481
|
+
SightSpanExporter,
|
|
482
|
+
initSight,
|
|
483
|
+
trackSolanaTransaction
|
|
484
|
+
};
|
|
175
485
|
//# sourceMappingURL=index.js.map
|