@goharvest/simforge 0.4.7 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +152 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +342 -219
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +857 -62
- package/dist/client.test.js.map +1 -1
- package/dist/http.d.ts +73 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +192 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/serialize.d.ts +55 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +68 -0
- package/dist/serialize.js.map +1 -0
- package/dist/serialize.test.d.ts +8 -0
- package/dist/serialize.test.d.ts.map +1 -0
- package/dist/serialize.test.js +226 -0
- package/dist/serialize.test.js.map +1 -0
- package/dist/tracing.d.ts +6 -5
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +32 -126
- package/dist/tracing.js.map +1 -1
- package/dist/tracing.test.js +1 -1
- package/dist/tracing.test.js.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +4 -2
package/dist/client.js
CHANGED
|
@@ -1,57 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Simforge client for provider-based API calls.
|
|
3
3
|
*/
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
5
|
import { runFunctionWithBaml, } from "./baml.js";
|
|
5
|
-
import {
|
|
6
|
+
import { DEFAULT_SERVICE_URL } from "./constants.js";
|
|
7
|
+
import { HttpClient, SimforgeError } from "./http.js";
|
|
8
|
+
import { serializeValue } from "./serialize.js";
|
|
6
9
|
import { SimforgeOpenAITracingProcessor } from "./tracing.js";
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
const pendingTracePromises = new Set();
|
|
10
|
+
// Zone.js property key for storing span stack
|
|
11
|
+
const SPAN_STACK_KEY = "simforgeSpanStack";
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
* The promise will be removed from tracking when it completes (success or failure).
|
|
13
|
-
* Useful for fire-and-forget operations that need to complete before process exit.
|
|
13
|
+
* Async context propagation for nested spans.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Uses AsyncLocalStorage in Node.js (preferred) or Zone.js in browsers
|
|
16
|
+
* to track parent-child relationships across concurrent async operations.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
return promise;
|
|
18
|
+
let asyncLocalStorage = null;
|
|
19
|
+
// Try to use AsyncLocalStorage (Node.js)
|
|
20
|
+
try {
|
|
21
|
+
asyncLocalStorage = new AsyncLocalStorage();
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.versions.node != null) {
|
|
30
|
-
let isFlushing = false;
|
|
31
|
-
process.on("beforeExit", () => {
|
|
32
|
-
if (pendingTracePromises.size > 0 && !isFlushing) {
|
|
33
|
-
isFlushing = true;
|
|
34
|
-
// Wait for all pending traces to complete
|
|
35
|
-
// This keeps the event loop alive until promises resolve
|
|
36
|
-
Promise.allSettled(Array.from(pendingTracePromises).map((p) => p.catch(() => {
|
|
37
|
-
// Silently ignore individual trace failures
|
|
38
|
-
})))
|
|
39
|
-
.then(() => {
|
|
40
|
-
isFlushing = false;
|
|
41
|
-
})
|
|
42
|
-
.catch(() => {
|
|
43
|
-
isFlushing = false;
|
|
44
|
-
});
|
|
45
|
-
}
|
|
23
|
+
catch {
|
|
24
|
+
// Not in Node.js - Zone.js will be used via globalThis.Zone if available
|
|
25
|
+
import("zone.js").catch(() => {
|
|
26
|
+
// zone.js not available, will fall back to no context tracking
|
|
46
27
|
});
|
|
47
28
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Get the current span stack from async context.
|
|
31
|
+
* Uses AsyncLocalStorage in Node.js, Zone.js in browsers.
|
|
32
|
+
*/
|
|
33
|
+
function getSpanStack() {
|
|
34
|
+
// Node.js: use AsyncLocalStorage
|
|
35
|
+
if (asyncLocalStorage) {
|
|
36
|
+
return asyncLocalStorage.getStore() ?? [];
|
|
53
37
|
}
|
|
38
|
+
// Browser: use Zone.js
|
|
39
|
+
const zone = globalThis.Zone;
|
|
40
|
+
if (zone) {
|
|
41
|
+
return zone.current.get(SPAN_STACK_KEY) ?? [];
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
54
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Run a function with a new span stack context.
|
|
47
|
+
* Uses AsyncLocalStorage in Node.js, Zone.js in browsers.
|
|
48
|
+
*/
|
|
49
|
+
function runWithSpanStack(stack, fn) {
|
|
50
|
+
// Node.js: use AsyncLocalStorage
|
|
51
|
+
if (asyncLocalStorage) {
|
|
52
|
+
return asyncLocalStorage.run(stack, fn);
|
|
53
|
+
}
|
|
54
|
+
// Browser: use Zone.js
|
|
55
|
+
const zone = globalThis.Zone;
|
|
56
|
+
if (zone) {
|
|
57
|
+
const childZone = zone.current.fork({
|
|
58
|
+
name: "simforgeSpan",
|
|
59
|
+
properties: { [SPAN_STACK_KEY]: stack },
|
|
60
|
+
});
|
|
61
|
+
return childZone.run(fn);
|
|
62
|
+
}
|
|
63
|
+
// Fallback (should not reach here if zone.js is loaded)
|
|
64
|
+
return fn();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get a handle to the current active span.
|
|
68
|
+
*
|
|
69
|
+
* Call this from inside a traced function (wrapped with `withSpan`) to get
|
|
70
|
+
* a span handle that allows setting metadata at runtime.
|
|
71
|
+
*
|
|
72
|
+
* Returns `null` if called outside of a span context.
|
|
73
|
+
*/
|
|
74
|
+
export function getCurrentSpan() {
|
|
75
|
+
const stack = getSpanStack();
|
|
76
|
+
const current = stack[stack.length - 1];
|
|
77
|
+
if (!current) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
setMetadata(metadata) {
|
|
82
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
current.metadata = { ...current.metadata, ...metadata };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Re-export SimforgeError for backwards compatibility
|
|
90
|
+
export { SimforgeError };
|
|
55
91
|
/**
|
|
56
92
|
* Client for making provider-based API calls via BAML.
|
|
57
93
|
*/
|
|
@@ -66,7 +102,19 @@ export class Simforge {
|
|
|
66
102
|
this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
|
|
67
103
|
this.timeout = config.timeout ?? 120000;
|
|
68
104
|
this.envVars = config.envVars ?? {};
|
|
69
|
-
|
|
105
|
+
const enabled = config.enabled ?? true;
|
|
106
|
+
if (enabled && (!config.apiKey || config.apiKey.trim() === "")) {
|
|
107
|
+
console.warn("Simforge: apiKey is empty — tracing is disabled. Provide a valid API key to enable tracing.");
|
|
108
|
+
this.enabled = false;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.enabled = enabled;
|
|
112
|
+
}
|
|
113
|
+
this.httpClient = new HttpClient({
|
|
114
|
+
apiKey: this.apiKey,
|
|
115
|
+
serviceUrl: this.serviceUrl,
|
|
116
|
+
timeout: this.timeout,
|
|
117
|
+
});
|
|
70
118
|
}
|
|
71
119
|
/**
|
|
72
120
|
* Fetch the function with its current version and BAML prompt from the server.
|
|
@@ -76,57 +124,16 @@ export class Simforge {
|
|
|
76
124
|
* @throws {SimforgeError} If the function is not found or an error occurs
|
|
77
125
|
*/
|
|
78
126
|
async fetchFunctionVersion(methodName) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const response = await fetch(url, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: {
|
|
87
|
-
"Content-Type": "application/json",
|
|
88
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
89
|
-
},
|
|
90
|
-
body: JSON.stringify(payload),
|
|
91
|
-
signal: controller.signal,
|
|
92
|
-
});
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
const errorText = await response.text();
|
|
95
|
-
throw new SimforgeError(`HTTP ${response.status}: ${errorText.slice(0, 500)}`);
|
|
96
|
-
}
|
|
97
|
-
const result = await response.json();
|
|
98
|
-
// Check if function was not found
|
|
99
|
-
if (result.id === null) {
|
|
100
|
-
throw new SimforgeError(`Function "${methodName}" not found. Create it at: ${this.serviceUrl}/functions`, "/functions");
|
|
101
|
-
}
|
|
102
|
-
// Check if function has no prompt
|
|
103
|
-
if (!result.prompt) {
|
|
104
|
-
throw new SimforgeError(`Function "${methodName}" has no prompt configured. Add one at: ${this.serviceUrl}/functions/${result.id}`, `/functions/${result.id}`);
|
|
105
|
-
}
|
|
106
|
-
// Check for errors in the response
|
|
107
|
-
if (result.error) {
|
|
108
|
-
if (result.url) {
|
|
109
|
-
throw new SimforgeError(`${result.error} Configure it at: ${this.serviceUrl}${result.url}`, result.url);
|
|
110
|
-
}
|
|
111
|
-
throw new SimforgeError(result.error);
|
|
112
|
-
}
|
|
113
|
-
return result;
|
|
127
|
+
const result = await this.httpClient.lookupFunction(methodName);
|
|
128
|
+
// Check if function was not found
|
|
129
|
+
if (result.id === null) {
|
|
130
|
+
throw new SimforgeError(`Function "${methodName}" not found. Create it at: ${this.serviceUrl}/functions`, "/functions");
|
|
114
131
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
if (error instanceof Error) {
|
|
120
|
-
if (error.name === "AbortError") {
|
|
121
|
-
throw new SimforgeError(`Request timed out after ${this.timeout}ms`);
|
|
122
|
-
}
|
|
123
|
-
throw new SimforgeError(error.message);
|
|
124
|
-
}
|
|
125
|
-
throw new SimforgeError("Unknown error occurred");
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
clearTimeout(timeoutId);
|
|
132
|
+
// Check if function has no prompt
|
|
133
|
+
if (!result.prompt) {
|
|
134
|
+
throw new SimforgeError(`Function "${methodName}" has no prompt configured. Add one at: ${this.serviceUrl}/functions/${result.id}`, `/functions/${result.id}`);
|
|
129
135
|
}
|
|
136
|
+
return result;
|
|
130
137
|
}
|
|
131
138
|
/**
|
|
132
139
|
* Call a method with the given named arguments via BAML execution.
|
|
@@ -137,144 +144,32 @@ export class Simforge {
|
|
|
137
144
|
* @throws {SimforgeError} If service_url is not set, or if an error occurs
|
|
138
145
|
*/
|
|
139
146
|
async call(methodName, inputs = {}) {
|
|
140
|
-
// If executeLocally is true, fetch the BAML and execute it locally
|
|
141
|
-
if (this.executeLocally) {
|
|
142
|
-
try {
|
|
143
|
-
const functionVersion = await this.fetchFunctionVersion(methodName);
|
|
144
|
-
const executionResult = await runFunctionWithBaml(functionVersion.prompt, inputs, functionVersion.providers, this.envVars);
|
|
145
|
-
// Create trace for the local execution
|
|
146
|
-
const resultStr = typeof executionResult.result === "string"
|
|
147
|
-
? executionResult.result
|
|
148
|
-
: JSON.stringify(executionResult.result);
|
|
149
|
-
// Create trace in background so user doesn't have to wait
|
|
150
|
-
// Track the promise to prevent garbage collection
|
|
151
|
-
void awaitOnExit(this.createTrace({
|
|
152
|
-
functionId: functionVersion.id,
|
|
153
|
-
result: resultStr,
|
|
154
|
-
inputs: Object.keys(inputs).length > 0 ? inputs : undefined,
|
|
155
|
-
rawCollector: executionResult.rawCollector,
|
|
156
|
-
})).catch(() => {
|
|
157
|
-
console.error("Failed to create trace");
|
|
158
|
-
// Silently ignore trace creation failures
|
|
159
|
-
});
|
|
160
|
-
return executionResult.result;
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
if (error instanceof SimforgeError) {
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
if (error instanceof Error) {
|
|
167
|
-
throw new SimforgeError(error.message);
|
|
168
|
-
}
|
|
169
|
-
throw new SimforgeError("Unknown error occurred during local execution");
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// Otherwise, fall back to server-side execution
|
|
173
|
-
const url = `${this.serviceUrl}/api/sdk/call`;
|
|
174
|
-
const payload = {
|
|
175
|
-
name: methodName,
|
|
176
|
-
inputs,
|
|
177
|
-
sdkVersion: __version__,
|
|
178
|
-
};
|
|
179
|
-
const controller = new AbortController();
|
|
180
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
181
147
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Check for errors in the response
|
|
197
|
-
if (result.error) {
|
|
198
|
-
if (result.url) {
|
|
199
|
-
throw new SimforgeError(`${result.error} Configure it at: ${this.serviceUrl}${result.url}`, result.url);
|
|
200
|
-
}
|
|
201
|
-
throw new SimforgeError(result.error);
|
|
202
|
-
}
|
|
203
|
-
return result.result;
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
if (error instanceof SimforgeError) {
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
209
|
-
if (error instanceof Error) {
|
|
210
|
-
if (error.name === "AbortError") {
|
|
211
|
-
throw new SimforgeError(`Request timed out after ${this.timeout}ms`);
|
|
212
|
-
}
|
|
213
|
-
throw new SimforgeError(error.message);
|
|
214
|
-
}
|
|
215
|
-
throw new SimforgeError("Unknown error occurred");
|
|
216
|
-
}
|
|
217
|
-
finally {
|
|
218
|
-
clearTimeout(timeoutId);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Create a trace from a local execution result.
|
|
223
|
-
* Internal method to record traces when executing BAML locally.
|
|
224
|
-
*/
|
|
225
|
-
async createTrace(params) {
|
|
226
|
-
const url = `${this.serviceUrl}/api/sdk/functions/${params.functionId}/traces`;
|
|
227
|
-
const payload = {
|
|
228
|
-
result: params.result,
|
|
229
|
-
source: params.source ?? "typescript-sdk",
|
|
230
|
-
sdkVersion: __version__,
|
|
231
|
-
};
|
|
232
|
-
if (params.inputs !== undefined) {
|
|
233
|
-
payload.inputs = params.inputs;
|
|
234
|
-
}
|
|
235
|
-
if (params.rawCollector !== undefined && params.rawCollector !== null) {
|
|
236
|
-
payload.rawCollector = params.rawCollector;
|
|
237
|
-
}
|
|
238
|
-
const controller = new AbortController();
|
|
239
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
240
|
-
try {
|
|
241
|
-
const response = await fetch(url, {
|
|
242
|
-
method: "POST",
|
|
243
|
-
headers: {
|
|
244
|
-
"Content-Type": "application/json",
|
|
245
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
246
|
-
},
|
|
247
|
-
body: JSON.stringify(payload),
|
|
248
|
-
signal: controller.signal,
|
|
148
|
+
const functionVersion = await this.fetchFunctionVersion(methodName);
|
|
149
|
+
const executionResult = await runFunctionWithBaml(functionVersion.prompt, inputs, functionVersion.providers, this.envVars);
|
|
150
|
+
// Create trace for the local execution
|
|
151
|
+
const resultStr = typeof executionResult.result === "string"
|
|
152
|
+
? executionResult.result
|
|
153
|
+
: JSON.stringify(executionResult.result);
|
|
154
|
+
// Create trace in background so user doesn't have to wait
|
|
155
|
+
this.httpClient.sendInternalTrace(functionVersion.id, {
|
|
156
|
+
result: resultStr,
|
|
157
|
+
source: "typescript-sdk",
|
|
158
|
+
...(Object.keys(inputs).length > 0 && { inputs }),
|
|
159
|
+
...(executionResult.rawCollector != null && {
|
|
160
|
+
rawCollector: executionResult.rawCollector,
|
|
161
|
+
}),
|
|
249
162
|
});
|
|
250
|
-
|
|
251
|
-
const errorText = await response.text();
|
|
252
|
-
throw new SimforgeError(`HTTP ${response.status}: ${errorText.slice(0, 500)}`);
|
|
253
|
-
}
|
|
254
|
-
const result = await response.json();
|
|
255
|
-
// Check for errors in the response
|
|
256
|
-
if (result.error) {
|
|
257
|
-
if (result.url) {
|
|
258
|
-
throw new SimforgeError(`${result.error} Configure it at: ${this.serviceUrl}${result.url}`, result.url);
|
|
259
|
-
}
|
|
260
|
-
throw new SimforgeError(result.error);
|
|
261
|
-
}
|
|
262
|
-
return result;
|
|
163
|
+
return executionResult.result;
|
|
263
164
|
}
|
|
264
165
|
catch (error) {
|
|
265
166
|
if (error instanceof SimforgeError) {
|
|
266
167
|
throw error;
|
|
267
168
|
}
|
|
268
169
|
if (error instanceof Error) {
|
|
269
|
-
if (error.name === "AbortError") {
|
|
270
|
-
throw new SimforgeError("Request timed out after 30000ms");
|
|
271
|
-
}
|
|
272
170
|
throw new SimforgeError(error.message);
|
|
273
171
|
}
|
|
274
|
-
throw new SimforgeError("Unknown error occurred");
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
clearTimeout(timeoutId);
|
|
172
|
+
throw new SimforgeError("Unknown error occurred during local execution");
|
|
278
173
|
}
|
|
279
174
|
}
|
|
280
175
|
/**
|
|
@@ -300,5 +195,233 @@ export class Simforge {
|
|
|
300
195
|
serviceUrl: this.serviceUrl,
|
|
301
196
|
});
|
|
302
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Wrap a function to automatically create a span for its inputs and outputs.
|
|
200
|
+
*
|
|
201
|
+
* The wrapped function behaves identically to the original, but sends
|
|
202
|
+
* span data to Simforge in the background after each call.
|
|
203
|
+
*
|
|
204
|
+
* Example usage:
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const client = new Simforge({ apiKey: 'your-api-key' });
|
|
207
|
+
*
|
|
208
|
+
* async function processOrder(orderId: string, items: string[]): Promise<{ total: number }> {
|
|
209
|
+
* // ... process order
|
|
210
|
+
* return { total: 100 };
|
|
211
|
+
* }
|
|
212
|
+
*
|
|
213
|
+
* // Basic usage (defaults to "custom" span type)
|
|
214
|
+
* const tracedProcessOrder = client.withSpan('order-processing', processOrder);
|
|
215
|
+
*
|
|
216
|
+
* // With explicit span type
|
|
217
|
+
* const tracedProcessOrder = client.withSpan('order-processing', { type: 'function' }, processOrder);
|
|
218
|
+
*
|
|
219
|
+
* // Call the wrapped function normally
|
|
220
|
+
* const result = await tracedProcessOrder('order-123', ['item-1', 'item-2']);
|
|
221
|
+
* // Span is automatically sent to Simforge
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @param traceFunctionKey - A string identifier for grouping spans (e.g., 'order-processing', 'user-auth')
|
|
225
|
+
* @param optionsOrFn - Either SpanOptions or the function to wrap
|
|
226
|
+
* @param maybeFn - The function to wrap if options were provided
|
|
227
|
+
* @returns A wrapped function with the same signature that creates spans for inputs and outputs
|
|
228
|
+
*/
|
|
229
|
+
withSpan(traceFunctionKey, optionsOrFn, maybeFn) {
|
|
230
|
+
if (!this.enabled) {
|
|
231
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
232
|
+
return fn;
|
|
233
|
+
}
|
|
234
|
+
// Handle overloaded signature
|
|
235
|
+
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
236
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
237
|
+
const self = this;
|
|
238
|
+
return function (...args) {
|
|
239
|
+
// Get current span stack to determine trace context
|
|
240
|
+
const currentStack = getSpanStack();
|
|
241
|
+
const parentContext = currentStack[currentStack.length - 1];
|
|
242
|
+
// Generate trace ID (new if root, inherit if nested)
|
|
243
|
+
const traceId = parentContext?.traceId ?? crypto.randomUUID();
|
|
244
|
+
const spanId = crypto.randomUUID();
|
|
245
|
+
const parentSpanId = parentContext?.spanId ?? null;
|
|
246
|
+
// Create new context for this span
|
|
247
|
+
const newContext = { traceId, spanId };
|
|
248
|
+
const newStack = [...currentStack, newContext];
|
|
249
|
+
// Capture inputs and start time
|
|
250
|
+
const inputs = args;
|
|
251
|
+
const startedAt = new Date().toISOString();
|
|
252
|
+
// Shared span parameters
|
|
253
|
+
const functionName = fn.name !== "" ? fn.name : undefined;
|
|
254
|
+
const baseSpanParams = {
|
|
255
|
+
traceFunctionKey,
|
|
256
|
+
functionName,
|
|
257
|
+
spanName: options.name ?? functionName ?? traceFunctionKey,
|
|
258
|
+
traceId,
|
|
259
|
+
spanId,
|
|
260
|
+
parentSpanId,
|
|
261
|
+
inputs,
|
|
262
|
+
startedAt,
|
|
263
|
+
spanType: options.type ?? "custom",
|
|
264
|
+
metadata: options.metadata,
|
|
265
|
+
};
|
|
266
|
+
// Helper to send span in background (fire-and-forget).
|
|
267
|
+
// Wrapped in try/catch so span errors never crash the host app.
|
|
268
|
+
const sendSpan = (params) => {
|
|
269
|
+
try {
|
|
270
|
+
// Merge runtime metadata (from setSpanMetadata) with definition-time metadata
|
|
271
|
+
const runtimeMetadata = newContext.metadata;
|
|
272
|
+
const mergedMetadata = runtimeMetadata
|
|
273
|
+
? { ...options.metadata, ...runtimeMetadata }
|
|
274
|
+
: options.metadata;
|
|
275
|
+
self.sendWrapperSpan({
|
|
276
|
+
...baseSpanParams,
|
|
277
|
+
...params,
|
|
278
|
+
metadata: mergedMetadata,
|
|
279
|
+
endedAt: new Date().toISOString(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Silently ignore — user's result/exception takes priority
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
// Execute function within new span context
|
|
287
|
+
const executeWithContext = () => {
|
|
288
|
+
const result = fn(...args);
|
|
289
|
+
// Check if result is a Promise (async function)
|
|
290
|
+
if (result instanceof Promise) {
|
|
291
|
+
return result
|
|
292
|
+
.then((resolvedResult) => {
|
|
293
|
+
sendSpan({ result: resolvedResult });
|
|
294
|
+
return resolvedResult;
|
|
295
|
+
})
|
|
296
|
+
.catch((error) => {
|
|
297
|
+
sendSpan({
|
|
298
|
+
result: undefined,
|
|
299
|
+
error: error instanceof Error ? error.message : String(error),
|
|
300
|
+
});
|
|
301
|
+
throw error;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// Sync function - create span in background
|
|
305
|
+
sendSpan({ result });
|
|
306
|
+
return result;
|
|
307
|
+
};
|
|
308
|
+
return runWithSpanStack(newStack, executeWithContext);
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get a function wrapper for a specific trace function key.
|
|
313
|
+
*
|
|
314
|
+
* This provides a fluent API alternative to calling withSpan directly,
|
|
315
|
+
* allowing you to bind the traceFunctionKey once and wrap multiple functions.
|
|
316
|
+
*
|
|
317
|
+
* Example usage:
|
|
318
|
+
* ```typescript
|
|
319
|
+
* const client = new Simforge({ apiKey: 'your-api-key' });
|
|
320
|
+
*
|
|
321
|
+
* const orderFunc = client.getFunction('order-processing');
|
|
322
|
+
* const tracedProcessOrder = orderFunc.withSpan(processOrder);
|
|
323
|
+
* const tracedValidateOrder = orderFunc.withSpan(validateOrder);
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @param traceFunctionKey - A string identifier for grouping spans
|
|
327
|
+
* @returns A SimforgeFunction instance for wrapping functions
|
|
328
|
+
*/
|
|
329
|
+
getFunction(traceFunctionKey) {
|
|
330
|
+
return new SimforgeFunction(this, traceFunctionKey);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Send a wrapper span from function execution.
|
|
334
|
+
* Internal method to record spans when using withSpan.
|
|
335
|
+
* Fire-and-forget - sends to externalSpans endpoint via httpClient.
|
|
336
|
+
*/
|
|
337
|
+
sendWrapperSpan(params) {
|
|
338
|
+
// Serialize inputs and result with superjson for type preservation
|
|
339
|
+
const serializedInputs = serializeValue(params.inputs);
|
|
340
|
+
const serializedResult = serializeValue(params.result);
|
|
341
|
+
// Format as an external span with the wrapper format
|
|
342
|
+
const externalSpan = {
|
|
343
|
+
id: params.spanId,
|
|
344
|
+
trace_id: params.traceId,
|
|
345
|
+
started_at: params.startedAt,
|
|
346
|
+
ended_at: params.endedAt,
|
|
347
|
+
span_data: {
|
|
348
|
+
name: params.spanName,
|
|
349
|
+
type: params.spanType,
|
|
350
|
+
input: serializedInputs.json,
|
|
351
|
+
output: serializedResult.json,
|
|
352
|
+
// Include superjson meta for type preservation
|
|
353
|
+
...(serializedInputs.meta !== undefined && {
|
|
354
|
+
input_meta: serializedInputs.meta,
|
|
355
|
+
}),
|
|
356
|
+
...(serializedResult.meta !== undefined && {
|
|
357
|
+
output_meta: serializedResult.meta,
|
|
358
|
+
}),
|
|
359
|
+
...(params.functionName !== undefined && {
|
|
360
|
+
function_name: params.functionName,
|
|
361
|
+
}),
|
|
362
|
+
...(params.error !== undefined && { error: params.error }),
|
|
363
|
+
...(params.metadata !== undefined && { metadata: params.metadata }),
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
// Add parent_id for nested spans
|
|
367
|
+
if (params.parentSpanId) {
|
|
368
|
+
externalSpan.parent_id = params.parentSpanId;
|
|
369
|
+
}
|
|
370
|
+
this.httpClient.sendExternalSpan({
|
|
371
|
+
type: "sdk-function",
|
|
372
|
+
source: "typescript-sdk-function",
|
|
373
|
+
sourceTraceId: params.traceId,
|
|
374
|
+
traceFunctionKey: params.traceFunctionKey,
|
|
375
|
+
rawSpan: externalSpan,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Represents a Simforge function that can wrap user functions for tracing.
|
|
381
|
+
*
|
|
382
|
+
* This provides a fluent API for binding a traceFunctionKey once and
|
|
383
|
+
* then wrapping multiple functions with that key.
|
|
384
|
+
*
|
|
385
|
+
* Example usage:
|
|
386
|
+
* ```typescript
|
|
387
|
+
* const client = new Simforge({ apiKey: 'your-api-key' });
|
|
388
|
+
*
|
|
389
|
+
* const orderFunc = client.getFunction('order-processing');
|
|
390
|
+
* const tracedProcessOrder = orderFunc.withSpan(processOrder);
|
|
391
|
+
* const tracedValidateOrder = orderFunc.withSpan(validateOrder);
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
export class SimforgeFunction {
|
|
395
|
+
constructor(client, traceFunctionKey) {
|
|
396
|
+
this.client = client;
|
|
397
|
+
this.traceFunctionKey = traceFunctionKey;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Wrap a function to automatically create a span for its inputs and outputs.
|
|
401
|
+
*
|
|
402
|
+
* The wrapped function behaves identically to the original, but sends
|
|
403
|
+
* span data to Simforge in the background after each call.
|
|
404
|
+
*
|
|
405
|
+
* Example usage:
|
|
406
|
+
* ```typescript
|
|
407
|
+
* const orderFunc = client.getFunction('order-processing');
|
|
408
|
+
*
|
|
409
|
+
* // Basic usage (defaults to "custom" span type)
|
|
410
|
+
* const tracedProcessOrder = orderFunc.withSpan(processOrder);
|
|
411
|
+
*
|
|
412
|
+
* // With explicit span type
|
|
413
|
+
* const tracedProcessOrder = orderFunc.withSpan({ type: 'function' }, processOrder);
|
|
414
|
+
* ```
|
|
415
|
+
*
|
|
416
|
+
* @param optionsOrFn - Either SpanOptions or the function to wrap
|
|
417
|
+
* @param maybeFn - The function to wrap if options were provided
|
|
418
|
+
* @returns A wrapped function with the same signature that creates spans
|
|
419
|
+
*/
|
|
420
|
+
withSpan(optionsOrFn, maybeFn) {
|
|
421
|
+
// Handle overloaded signature
|
|
422
|
+
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
423
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
424
|
+
return this.client.withSpan(this.traceFunctionKey, options, fn);
|
|
425
|
+
}
|
|
303
426
|
}
|
|
304
427
|
//# sourceMappingURL=client.js.map
|