@thesight/sdk 0.1.1 → 0.3.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/dist/index.d.ts CHANGED
@@ -1,14 +1,21 @@
1
- import { Connection, ConnectionConfig, Commitment, Transaction, VersionedTransaction, SendOptions } from '@solana/web3.js';
1
+ import { TransactionSignature, Connection, Finality, ConnectionConfig, Commitment, SendOptions } from '@solana/web3.js';
2
2
  import { Tracer } from '@opentelemetry/api';
3
- import { AnchorIdl, CpiTree, DecodedError } from '@thesight/core';
3
+ import { IdlResolver, AnchorIdl } from '@thesight/core';
4
4
  export { AnchorIdl, CpiTree, CuAttribution, DecodedError, FlamegraphItem, IdlResolver } from '@thesight/core';
5
5
  import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
6
6
  import { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
7
7
  import { ExportResult } from '@opentelemetry/core';
8
8
 
9
9
  interface InitSightConfig {
10
- /** Project API key (`sk_live_...`) from the Sight dashboard. */
11
- apiKey: string;
10
+ /**
11
+ * Sight DSN — one string encoding both the ingest endpoint and the API key.
12
+ *
13
+ * Format: `https://<apiKey>@<host>/ingest`
14
+ *
15
+ * Get yours from the Sight dashboard when you create a project. For local
16
+ * dev against a Docker compose stack: `http://sk_live_xxx@localhost:3001/ingest`
17
+ */
18
+ dsn: string;
12
19
  /**
13
20
  * Human-readable service name that shows up on every span. Pick something
14
21
  * that identifies this process — `'swap-bot'`, `'market-maker'`,
@@ -17,12 +24,6 @@ interface InitSightConfig {
17
24
  serviceName: string;
18
25
  /** Optional version string for the service. Useful for correlating spans with a deploy. */
19
26
  serviceVersion?: string;
20
- /**
21
- * Full ingest URL. Defaults to the hosted Sight ingest. Override for
22
- * local development (e.g. `http://localhost:3001/ingest`) or self-hosted
23
- * deployments.
24
- */
25
- ingestUrl?: string;
26
27
  /** BatchSpanProcessor flush interval, in ms. Defaults to 5s. */
27
28
  batchDelayMs?: number;
28
29
  /** Max spans per HTTP POST. Clamped to 100 by the exporter. */
@@ -37,44 +38,30 @@ interface SightTracerHandle {
37
38
  }
38
39
  /**
39
40
  * Initialize Sight tracing. Call this **once** near the top of your
40
- * process entry point — typically in the same file that boots your
41
- * HTTP server or worker.
41
+ * process entry point.
42
42
  *
43
43
  * import { initSight, InstrumentedConnection } from '@thesight/sdk';
44
44
  *
45
45
  * initSight({
46
- * apiKey: process.env.SIGHT_API_KEY!,
46
+ * dsn: process.env.SIGHT_DSN!,
47
47
  * serviceName: 'swap-bot',
48
- * ingestUrl: process.env.SIGHT_INGEST_URL, // optional, defaults to prod
49
48
  * });
50
49
  *
51
50
  * const connection = new InstrumentedConnection(process.env.RPC_URL!);
52
- * const { signature } = await connection.sendAndConfirmInstrumented(tx);
53
- *
54
- * Registers a NodeTracerProvider as the global OTel tracer, so any call to
55
- * `trace.getTracer(...)` (including the ones inside `InstrumentedConnection`)
56
- * routes spans through the Sight exporter. Context propagation uses the
57
- * Node async hooks manager so spans nest correctly across await boundaries.
58
- *
59
- * Returns a handle with a `shutdown()` method. Call it at graceful shutdown
60
- * time so pending spans are flushed before the process exits:
51
+ * // Every sendRawTransaction call now emits a span automatically.
61
52
  *
62
- * const sight = initSight({ ... });
63
- * process.on('SIGTERM', async () => {
64
- * await sight.shutdown();
65
- * process.exit(0);
66
- * });
53
+ * Registers a NodeTracerProvider as the global OTel tracer. Context
54
+ * propagation uses Node async hooks so spans nest correctly across
55
+ * await boundaries.
67
56
  */
68
57
  declare function initSight(config: InitSightConfig): SightTracerHandle;
69
58
 
70
59
  interface SightExporterConfig {
71
- /** Project API key (`sk_live_...`). Sent as Bearer auth on every POST. */
72
- apiKey: string;
73
60
  /**
74
- * Full ingest URL including the path (e.g. `https://ingest.thesight.dev/ingest`
75
- * or `http://localhost:3001/ingest` for local dev).
61
+ * Sight DSN — one string encoding both the ingest endpoint and the API key.
62
+ * Format: https://<apiKey>@<host>/ingest
76
63
  */
77
- ingestUrl: string;
64
+ dsn: string;
78
65
  /** Inject a fake fetch in tests. Defaults to `globalThis.fetch`. */
79
66
  fetchImpl?: typeof fetch;
80
67
  /** Max spans per POST. Ingest enforces an upper bound of 100. */
@@ -108,14 +95,128 @@ declare class SightSpanExporter implements SpanExporter {
108
95
  private convertSpan;
109
96
  }
110
97
 
98
+ /**
99
+ * Parse a Sight DSN into its constituent parts.
100
+ *
101
+ * DSN format:
102
+ * https://<apiKey>@<host>[:<port>]/<path>
103
+ *
104
+ * Examples:
105
+ * https://sk_live_abc123@ingest.thesight.dev/ingest (production)
106
+ * http://sk_live_abc123@localhost:3001/ingest (local dev)
107
+ *
108
+ * The API key sits in the URL's username position. The SDK extracts it
109
+ * and sends it as a Bearer token to the ingest URL (with the key removed
110
+ * from the URL). This mirrors the Sentry DSN model — one string encodes
111
+ * both "where to send" and "who you are", so there's no separate
112
+ * apiKey + ingestUrl to misconfig independently.
113
+ */
114
+ interface ParsedDsn {
115
+ /** The API key extracted from the DSN's username position. */
116
+ apiKey: string;
117
+ /** The ingest endpoint URL with the API key stripped out. */
118
+ ingestUrl: string;
119
+ }
120
+ /**
121
+ * Parse a DSN string into `{ apiKey, ingestUrl }`.
122
+ *
123
+ * Throws with a clear message on:
124
+ * - Malformed URL
125
+ * - Missing API key (no username in the URL)
126
+ */
127
+ declare function parseDsn(dsn: string): ParsedDsn;
128
+ /**
129
+ * Build a DSN from its parts. Used by the dashboard when generating the
130
+ * DSN for a newly-created project.
131
+ */
132
+ declare function buildDsn(apiKey: string, ingestHost: string): string;
133
+
134
+ interface TrackTransactionOptions {
135
+ /** The signature returned from a prior `sendRawTransaction` / `sendTransaction` call. */
136
+ signature: TransactionSignature;
137
+ /**
138
+ * Any `@solana/web3.js` Connection — plain or instrumented. Used to fetch
139
+ * the on-chain transaction via `getTransaction` for enrichment. Does
140
+ * **not** need to be an `InstrumentedConnection`; this helper intentionally
141
+ * never touches the transaction-send path.
142
+ */
143
+ connection: Connection;
144
+ /**
145
+ * Service name used for the span. Falls back to the service name
146
+ * configured by `initSight`. Useful when you want a different service
147
+ * name for a specific subsystem's spans.
148
+ */
149
+ serviceName?: string;
150
+ /** OTel tracer override. Defaults to `trace.getTracer('@thesight/sdk')`. */
151
+ tracer?: Tracer;
152
+ /** Optional IDL resolver for program/error decoding. A fresh one is built if omitted. */
153
+ idlResolver?: IdlResolver;
154
+ /**
155
+ * Pre-registered IDLs to hand to a freshly-built resolver. Ignored when
156
+ * `idlResolver` is provided.
157
+ */
158
+ idls?: Record<string, AnchorIdl>;
159
+ /** Finality commitment used for the enrichment fetch. Default 'confirmed'. */
160
+ commitment?: Finality;
161
+ /** Max wall-time before the span is ended with status=timeout. Default 30000ms. */
162
+ timeoutMs?: number;
163
+ /** Base poll interval for retrying getTransaction. Default 500ms. */
164
+ pollIntervalMs?: number;
165
+ }
166
+ /**
167
+ * **Non-wrapping observation helper.** Given a transaction signature that
168
+ * was already sent through any `Connection`, fetch the on-chain record,
169
+ * parse the logs, and emit a single OpenTelemetry span describing the
170
+ * transaction.
171
+ *
172
+ * Unlike `InstrumentedConnection`, this helper **never touches the send
173
+ * path** — it only observes after the fact via `getTransaction(signature)`.
174
+ * Use this when:
175
+ *
176
+ * - You want observability for wallet-adapter flows where the send path
177
+ * goes through the wallet and you can't easily override the Connection
178
+ * - You're security-conscious and don't want any SDK code on the critical
179
+ * transaction path
180
+ * - You want to instrument a handful of high-value transactions rather
181
+ * than every call a Connection makes
182
+ *
183
+ * Usage:
184
+ *
185
+ * import { trackSolanaTransaction } from '@thesight/sdk';
186
+ * import { Connection } from '@solana/web3.js';
187
+ *
188
+ * const connection = new Connection(rpcUrl);
189
+ * const signature = await wallet.sendTransaction(tx, connection);
190
+ *
191
+ * // Fire-and-forget. Span flushes on its own.
192
+ * void trackSolanaTransaction({
193
+ * signature,
194
+ * connection,
195
+ * serviceName: 'checkout',
196
+ * idls: { [programId]: idl },
197
+ * });
198
+ *
199
+ * Returns a `Promise<void>` — typically not awaited, but callers who want
200
+ * to wait for the span to export (e.g. short-lived scripts) can await it.
201
+ */
202
+ declare function trackSolanaTransaction(opts: TrackTransactionOptions): Promise<void>;
203
+
111
204
  interface SightConfig {
112
- /** OTel tracer. Get via trace.getTracer('your-service') */
205
+ /** OTel tracer. Defaults to `trace.getTracer('@thesight/sdk')`. */
113
206
  tracer?: Tracer;
114
207
  /** Override RPC endpoint for IDL fetching (defaults to same as connection) */
115
208
  idlRpcEndpoint?: string;
116
- /** Commitment to use when fetching confirmed tx details */
209
+ /**
210
+ * Commitment used when fetching confirmed tx details for enrichment.
211
+ * Defaults to 'confirmed'.
212
+ */
117
213
  commitment?: Commitment;
118
- /** Skip IDL resolution (faster, no program names or error decoding) */
214
+ /**
215
+ * Skip IDL resolution entirely. Spans will still emit with signature and
216
+ * timing attributes, but program names, instruction names, CPI trees, and
217
+ * decoded errors will all be omitted. Useful when you want minimal
218
+ * overhead and don't care about the richer observability.
219
+ */
119
220
  skipIdlResolution?: boolean;
120
221
  /**
121
222
  * Pre-register IDLs at construction time. Keys are program IDs, values are
@@ -126,18 +227,35 @@ interface SightConfig {
126
227
  /**
127
228
  * Opt into reading Anchor IDL accounts from the on-chain PDA as a fallback
128
229
  * when a program is not explicitly registered. Defaults to **false** —
129
- * Sight's model is that IDLs are explicitly provided by the application
130
- * (via this option or `registerIdl`), not silently guessed from chain.
230
+ * Sight's model is that IDLs are explicitly provided by the application,
231
+ * not silently guessed from chain.
131
232
  */
132
233
  allowOnChainIdlFetch?: boolean;
234
+ /**
235
+ * Max wall-time the background enrichment task will wait for a
236
+ * transaction to show up in `getTransaction`. After this expires the
237
+ * span is ended with `solana.tx.status = 'timeout'`. Default 30000 (30s).
238
+ */
239
+ enrichmentTimeoutMs?: number;
240
+ /**
241
+ * How often the enrichment task polls `getTransaction`. Each retry backs
242
+ * off by 1.5x up to a cap. Default 500ms base.
243
+ */
244
+ enrichmentPollIntervalMs?: number;
245
+ /**
246
+ * Disable automatic span creation in the overridden `sendRawTransaction`.
247
+ * When true, InstrumentedConnection behaves like a plain Connection. You
248
+ * probably don't want this — it's here as an escape hatch for tests and
249
+ * rare cases where you need the class hierarchy but not the tracing.
250
+ */
251
+ disableAutoSpan?: boolean;
133
252
  }
134
253
  interface SightSpanAttributes {
135
254
  'solana.tx.signature': string;
136
- 'solana.tx.status': 'confirmed' | 'failed' | 'timeout';
255
+ 'solana.tx.status': 'submitted' | 'confirmed' | 'failed' | 'timeout';
137
256
  'solana.tx.slot'?: number;
138
257
  'solana.tx.fee_lamports'?: number;
139
258
  'solana.tx.submit_ms': number;
140
- 'solana.tx.confirmation_ms'?: number;
141
259
  'solana.tx.enrichment_ms'?: number;
142
260
  'solana.tx.cu_used'?: number;
143
261
  'solana.tx.cu_budget'?: number;
@@ -150,17 +268,41 @@ interface SightSpanAttributes {
150
268
  'solana.tx.error_msg'?: string;
151
269
  }
152
270
  /**
153
- * Drop-in replacement for @solana/web3.js Connection that instruments
154
- * every transaction with OpenTelemetry spans.
271
+ * Drop-in replacement for `@solana/web3.js`'s `Connection` that emits an
272
+ * OpenTelemetry span for **every** call to `sendRawTransaction`
273
+ * regardless of whether the caller is your own code, an Anchor provider's
274
+ * `sendAndConfirm`, `@solana/web3.js`'s top-level `sendAndConfirmTransaction`,
275
+ * a wallet adapter, or anything else.
276
+ *
277
+ * Internally it overrides the parent class's `sendRawTransaction`, so the
278
+ * span covers the same surface web3.js already mediates. No mutation of
279
+ * the transaction bytes — we call `super.sendRawTransaction(rawTx, opts)`
280
+ * verbatim and observe the signature it returns.
281
+ *
282
+ * After the signature is returned (synchronously from the caller's point
283
+ * of view), a background task polls `getTransaction` until the on-chain
284
+ * record is available, parses the program logs via `@thesight/core`, and
285
+ * enriches the span with CU attribution, per-CPI events, program + instruction
286
+ * names, and decoded error details. The background task never blocks the
287
+ * caller — if it fails or times out, the span ends with status=timeout and
288
+ * whatever partial data was collected.
155
289
  *
156
290
  * Usage:
157
- * const connection = new InstrumentedConnection(rpcUrl, {
158
- * tracer: trace.getTracer('my-service'),
159
- * });
160
291
  *
161
- * The Solana transaction becomes a child span of whatever OTel context
162
- * is active when sendAndConfirmTransaction is called — so it automatically
163
- * nests inside your HTTP handler or background job span.
292
+ * import { initSight, InstrumentedConnection } from '@thesight/sdk';
293
+ *
294
+ * initSight({ dsn: process.env.SIGHT_DSN!, serviceName: 'swap-bot' });
295
+ *
296
+ * // Anywhere you currently construct a Connection, construct this instead:
297
+ * const connection = new InstrumentedConnection(rpcUrl, {
298
+ * commitment: 'confirmed',
299
+ * });
300
+ *
301
+ * // All of the following now emit spans automatically:
302
+ * await connection.sendRawTransaction(tx.serialize());
303
+ * await sendAndConfirmTransaction(connection, tx, signers); // web3.js
304
+ * await program.methods.foo().rpc(); // Anchor
305
+ * await wallet.sendTransaction(tx, connection); // wallet adapter
164
306
  */
165
307
  declare class InstrumentedConnection extends Connection {
166
308
  private sightConfig;
@@ -179,16 +321,41 @@ declare class InstrumentedConnection extends Connection {
179
321
  /** Bulk-register multiple IDLs. */
180
322
  registerIdls(idls: Record<string, AnchorIdl>): void;
181
323
  /**
182
- * Sends and confirms a transaction, wrapped in an OTel span.
183
- * The span is a child of the current active context.
324
+ * Overrides the parent `Connection.sendRawTransaction` so every submit
325
+ * including those made by Anchor's `provider.sendAndConfirm`, web3.js's
326
+ * top-level `sendAndConfirmTransaction`, and wallet adapter flows — is
327
+ * observed by an OTel span automatically.
328
+ *
329
+ * The span is a child of whatever OTel context is active when the call
330
+ * fires, so app-level parent spans (HTTP handlers, job runs, wallet flow
331
+ * spans from another SDK) nest the Sight span correctly.
332
+ */
333
+ sendRawTransaction(rawTransaction: Buffer | Uint8Array | Array<number>, options?: SendOptions): Promise<TransactionSignature>;
334
+ /**
335
+ * Poll `getTransaction` until the on-chain record is available, then
336
+ * enrich the span with CU attribution, program names, decoded errors,
337
+ * and per-CPI events.
338
+ *
339
+ * This runs async and is never awaited by callers of `sendRawTransaction`.
340
+ * Failures inside this method attach to the span via recordException
341
+ * rather than throwing.
342
+ */
343
+ private enrichSpanInBackground;
344
+ /**
345
+ * Poll `getTransaction(signature)` until either the on-chain record is
346
+ * returned or the deadline passes. Exponential backoff (1.5x) capped at
347
+ * 2 seconds to balance responsiveness against RPC load.
348
+ */
349
+ private pollForTransaction;
350
+ /** Attach the flat fields (slot, fee, CU, status) from a tx response to a span. */
351
+ private attachTxDetailsToSpan;
352
+ /**
353
+ * Parse program logs into a CPI tree, enrich with registered IDLs, and
354
+ * emit one `cpi.invoke` event per invocation onto the span. Root
355
+ * program/instruction names are copied onto span attributes so dashboards
356
+ * can filter by them without walking events.
184
357
  */
185
- sendAndConfirmInstrumented(transaction: Transaction | VersionedTransaction, options?: SendOptions & {
186
- commitment?: Commitment;
187
- }): Promise<{
188
- signature: string;
189
- cpiTree?: CpiTree;
190
- error?: DecodedError;
191
- }>;
358
+ private attachParsedLogsToSpan;
192
359
  }
193
360
 
194
- export { type InitSightConfig, InstrumentedConnection, type SightConfig, type SightExporterConfig, type SightSpanAttributes, SightSpanExporter, type SightTracerHandle, initSight };
361
+ export { type InitSightConfig, InstrumentedConnection, type ParsedDsn, type SightConfig, type SightExporterConfig, type SightSpanAttributes, SightSpanExporter, type SightTracerHandle, type TrackTransactionOptions, buildDsn, initSight, parseDsn, trackSolanaTransaction };