@braintrust/temporal 0.1.0 → 0.1.1

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
@@ -33,9 +33,14 @@ const client = new Client({
33
33
  plugins: [plugin],
34
34
  });
35
35
 
36
+ // ESM-safe resolution (recommended for ESM projects):
37
+ const workflowsUrl = new URL("./workflows", import.meta.url);
38
+ const workflowsPath = workflowsUrl.pathname;
39
+ // CommonJS resolution (existing/example usage):
40
+ // const workflowsPath = require.resolve("./workflows");
36
41
  const worker = await Worker.create({
37
42
  taskQueue: "my-queue",
38
- workflowsPath: require.resolve("./workflows"),
43
+ workflowsPath: workflowsPath,
39
44
  activities,
40
45
  plugins: [plugin],
41
46
  });
@@ -0,0 +1,31 @@
1
+ // src/utils.ts
2
+ var BRAINTRUST_SPAN_HEADER = "_braintrust-span";
3
+ var BRAINTRUST_WORKFLOW_SPAN_HEADER = "_braintrust-workflow-span";
4
+ var BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = "_braintrust-workflow-span-id";
5
+ function serializeHeaderValue(value) {
6
+ return {
7
+ metadata: {
8
+ encoding: new TextEncoder().encode("json/plain")
9
+ },
10
+ data: new TextEncoder().encode(JSON.stringify(value))
11
+ };
12
+ }
13
+ function deserializeHeaderValue(payload) {
14
+ if (!payload?.data) {
15
+ return void 0;
16
+ }
17
+ try {
18
+ const decoded = new TextDecoder().decode(payload.data);
19
+ return JSON.parse(decoded);
20
+ } catch {
21
+ return void 0;
22
+ }
23
+ }
24
+
25
+ export {
26
+ BRAINTRUST_SPAN_HEADER,
27
+ BRAINTRUST_WORKFLOW_SPAN_HEADER,
28
+ BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
29
+ serializeHeaderValue,
30
+ deserializeHeaderValue
31
+ };
package/dist/index.d.mts CHANGED
@@ -43,8 +43,7 @@ declare class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
43
43
  configureClient(options: Omit<ClientOptions, "plugins">): Omit<ClientOptions, "plugins">;
44
44
  /**
45
45
  * Configure the Temporal Worker with Braintrust interceptors and sinks.
46
- * Adds the activity interceptor for creating spans, the sinks for workflow spans,
47
- * and the workflow interceptor modules for bundling.
46
+ * Prepends interceptors to ensure they run first, making plugin order irrelevant.
48
47
  */
49
48
  configureWorker(options: WorkerOptions): WorkerOptions;
50
49
  }
package/dist/index.d.ts CHANGED
@@ -43,8 +43,7 @@ declare class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
43
43
  configureClient(options: Omit<ClientOptions, "plugins">): Omit<ClientOptions, "plugins">;
44
44
  /**
45
45
  * Configure the Temporal Worker with Braintrust interceptors and sinks.
46
- * Adds the activity interceptor for creating spans, the sinks for workflow spans,
47
- * and the workflow interceptor modules for bundling.
46
+ * Prepends interceptors to ensure they run first, making plugin order irrelevant.
48
47
  */
49
48
  configureWorker(options: WorkerOptions): WorkerOptions;
50
49
  }
package/dist/index.js CHANGED
@@ -233,6 +233,7 @@ function createBraintrustActivityInterceptor(ctx) {
233
233
  }
234
234
 
235
235
  // src/plugin.ts
236
+ var WORKFLOW_INTERCEPTORS_SPEC = "@braintrust/temporal/workflow-interceptors";
236
237
  var BraintrustTemporalPlugin = class {
237
238
  get name() {
238
239
  return "braintrust";
@@ -246,11 +247,14 @@ var BraintrustTemporalPlugin = class {
246
247
  const braintrustInterceptor = createBraintrustClientInterceptor();
247
248
  let workflow;
248
249
  if (Array.isArray(existing)) {
249
- workflow = [...existing, braintrustInterceptor];
250
+ workflow = [
251
+ ...existing,
252
+ braintrustInterceptor
253
+ ];
250
254
  } else if (existing) {
251
255
  workflow = {
252
256
  ...existing,
253
- ...braintrustInterceptor
257
+ calls: [...existing.calls ?? [], () => braintrustInterceptor]
254
258
  };
255
259
  } else {
256
260
  workflow = [braintrustInterceptor];
@@ -265,28 +269,25 @@ var BraintrustTemporalPlugin = class {
265
269
  }
266
270
  /**
267
271
  * Configure the Temporal Worker with Braintrust interceptors and sinks.
268
- * Adds the activity interceptor for creating spans, the sinks for workflow spans,
269
- * and the workflow interceptor modules for bundling.
272
+ * Prepends interceptors to ensure they run first, making plugin order irrelevant.
270
273
  */
271
274
  configureWorker(options) {
272
- const existingActivityInterceptors = options.interceptors?.activity ?? [];
273
- const existingWorkflowModules = options.interceptors?.workflowModules ?? [];
274
- const existingSinks = options.sinks ?? {};
275
- const braintrustSinks = createBraintrustSinks();
276
- const workflowInterceptorsPath = require.resolve("@braintrust/temporal/workflow-interceptors");
277
275
  return {
278
276
  ...options,
279
277
  interceptors: {
280
278
  ...options.interceptors,
281
279
  activity: [
282
- ...existingActivityInterceptors,
283
- createBraintrustActivityInterceptor
280
+ createBraintrustActivityInterceptor,
281
+ ...options.interceptors?.activity ?? []
284
282
  ],
285
- workflowModules: [...existingWorkflowModules, workflowInterceptorsPath]
283
+ workflowModules: [
284
+ WORKFLOW_INTERCEPTORS_SPEC,
285
+ ...options.interceptors?.workflowModules ?? []
286
+ ]
286
287
  },
287
288
  sinks: {
288
- ...existingSinks,
289
- ...braintrustSinks
289
+ ...createBraintrustSinks(),
290
+ ...options.sinks ?? {}
290
291
  }
291
292
  };
292
293
  }
package/dist/index.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  BRAINTRUST_SPAN_HEADER,
3
3
  BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
4
- __require,
5
4
  deserializeHeaderValue
6
- } from "./chunk-KT66NY2E.mjs";
5
+ } from "./chunk-NKP2TK53.mjs";
7
6
 
8
7
  // src/interceptors.ts
9
8
  import { defaultPayloadConverter } from "@temporalio/common";
@@ -188,6 +187,7 @@ function createBraintrustActivityInterceptor(ctx) {
188
187
  }
189
188
 
190
189
  // src/plugin.ts
190
+ var WORKFLOW_INTERCEPTORS_SPEC = "@braintrust/temporal/workflow-interceptors";
191
191
  var BraintrustTemporalPlugin = class {
192
192
  get name() {
193
193
  return "braintrust";
@@ -201,11 +201,14 @@ var BraintrustTemporalPlugin = class {
201
201
  const braintrustInterceptor = createBraintrustClientInterceptor();
202
202
  let workflow;
203
203
  if (Array.isArray(existing)) {
204
- workflow = [...existing, braintrustInterceptor];
204
+ workflow = [
205
+ ...existing,
206
+ braintrustInterceptor
207
+ ];
205
208
  } else if (existing) {
206
209
  workflow = {
207
210
  ...existing,
208
- ...braintrustInterceptor
211
+ calls: [...existing.calls ?? [], () => braintrustInterceptor]
209
212
  };
210
213
  } else {
211
214
  workflow = [braintrustInterceptor];
@@ -220,30 +223,25 @@ var BraintrustTemporalPlugin = class {
220
223
  }
221
224
  /**
222
225
  * Configure the Temporal Worker with Braintrust interceptors and sinks.
223
- * Adds the activity interceptor for creating spans, the sinks for workflow spans,
224
- * and the workflow interceptor modules for bundling.
226
+ * Prepends interceptors to ensure they run first, making plugin order irrelevant.
225
227
  */
226
228
  configureWorker(options) {
227
- const existingActivityInterceptors = options.interceptors?.activity ?? [];
228
- const existingWorkflowModules = options.interceptors?.workflowModules ?? [];
229
- const existingSinks = options.sinks ?? {};
230
- const braintrustSinks = createBraintrustSinks();
231
- const workflowInterceptorsPath = __require.resolve(
232
- "@braintrust/temporal/workflow-interceptors"
233
- );
234
229
  return {
235
230
  ...options,
236
231
  interceptors: {
237
232
  ...options.interceptors,
238
233
  activity: [
239
- ...existingActivityInterceptors,
240
- createBraintrustActivityInterceptor
234
+ createBraintrustActivityInterceptor,
235
+ ...options.interceptors?.activity ?? []
241
236
  ],
242
- workflowModules: [...existingWorkflowModules, workflowInterceptorsPath]
237
+ workflowModules: [
238
+ WORKFLOW_INTERCEPTORS_SPEC,
239
+ ...options.interceptors?.workflowModules ?? []
240
+ ]
243
241
  },
244
242
  sinks: {
245
- ...existingSinks,
246
- ...braintrustSinks
243
+ ...createBraintrustSinks(),
244
+ ...options.sinks ?? {}
247
245
  }
248
246
  };
249
247
  }
@@ -4,7 +4,7 @@ import {
4
4
  BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
5
5
  deserializeHeaderValue,
6
6
  serializeHeaderValue
7
- } from "./chunk-KT66NY2E.mjs";
7
+ } from "./chunk-NKP2TK53.mjs";
8
8
 
9
9
  // src/workflow-interceptors.ts
10
10
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braintrust/temporal",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "SDK for integrating Braintrust with Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/index.ts CHANGED
@@ -24,10 +24,12 @@
24
24
  * plugins: [braintrustPlugin],
25
25
  * });
26
26
  *
27
+ * const workflowsUrl = new URL("./workflows", import.meta.url);
28
+ * const workflowsPath = workflowsUrl.pathname;
27
29
  * // Create worker with the plugin
28
30
  * const worker = await Worker.create({
29
31
  * taskQueue: "my-queue",
30
- * workflowsPath: require.resolve("./workflows"),
32
+ * workflowsPath: workflowsPath,
31
33
  * activities,
32
34
  * plugins: [braintrustPlugin],
33
35
  * });
package/src/plugin.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { ClientPlugin, ClientOptions } from "@temporalio/client";
1
+ import type {
2
+ ClientPlugin,
3
+ ClientOptions,
4
+ WorkflowClientInterceptor,
5
+ WorkflowClientInterceptors,
6
+ } from "@temporalio/client";
2
7
  import type { WorkerPlugin, WorkerOptions } from "@temporalio/worker";
3
8
  import {
4
9
  createBraintrustClientInterceptor,
@@ -6,6 +11,9 @@ import {
6
11
  } from "./interceptors";
7
12
  import { createBraintrustSinks } from "./sinks";
8
13
 
14
+ // Add the workflow interceptor package specifier so the Temporal bundler can include it
15
+ const WORKFLOW_INTERCEPTORS_SPEC = "@braintrust/temporal/workflow-interceptors";
16
+
9
17
  /**
10
18
  * A Braintrust plugin for Temporal that automatically instruments
11
19
  * workflows and activities with tracing spans.
@@ -53,17 +61,24 @@ export class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
53
61
  const existing = options.interceptors?.workflow;
54
62
  const braintrustInterceptor = createBraintrustClientInterceptor();
55
63
 
56
- // workflow can be an array or an object with named interceptors
57
- let workflow: typeof existing;
64
+ let workflow:
65
+ | WorkflowClientInterceptors
66
+ | WorkflowClientInterceptor[]
67
+ | undefined;
68
+
58
69
  if (Array.isArray(existing)) {
59
- workflow = [...existing, braintrustInterceptor];
70
+ workflow = [
71
+ ...(existing as WorkflowClientInterceptor[]),
72
+ braintrustInterceptor,
73
+ ];
60
74
  } else if (existing) {
61
- // It's a WorkflowClientInterceptors object, merge our interceptor
75
+ // It's a WorkflowClientInterceptors object, merge our interceptor into the calls array
62
76
  workflow = {
63
77
  ...existing,
64
- ...braintrustInterceptor,
78
+ calls: [...(existing.calls ?? []), () => braintrustInterceptor],
65
79
  };
66
80
  } else {
81
+ // keep in new array form
67
82
  workflow = [braintrustInterceptor];
68
83
  }
69
84
 
@@ -73,40 +88,30 @@ export class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
73
88
  ...options.interceptors,
74
89
  workflow,
75
90
  },
76
- };
91
+ } as Omit<ClientOptions, "plugins">;
77
92
  }
78
93
 
79
94
  /**
80
95
  * Configure the Temporal Worker with Braintrust interceptors and sinks.
81
- * Adds the activity interceptor for creating spans, the sinks for workflow spans,
82
- * and the workflow interceptor modules for bundling.
96
+ * Prepends interceptors to ensure they run first, making plugin order irrelevant.
83
97
  */
84
98
  configureWorker(options: WorkerOptions): WorkerOptions {
85
- const existingActivityInterceptors = options.interceptors?.activity ?? [];
86
- const existingWorkflowModules = options.interceptors?.workflowModules ?? [];
87
- const existingSinks = options.sinks ?? {};
88
-
89
- const braintrustSinks = createBraintrustSinks();
90
-
91
- // Resolve the workflow interceptors module path
92
- // This needs to be resolved at runtime to get the actual file path
93
- const workflowInterceptorsPath = require.resolve(
94
- "@braintrust/temporal/workflow-interceptors",
95
- );
96
-
97
99
  return {
98
100
  ...options,
99
101
  interceptors: {
100
102
  ...options.interceptors,
101
103
  activity: [
102
- ...existingActivityInterceptors,
103
104
  createBraintrustActivityInterceptor,
105
+ ...(options.interceptors?.activity ?? []),
106
+ ],
107
+ workflowModules: [
108
+ WORKFLOW_INTERCEPTORS_SPEC,
109
+ ...(options.interceptors?.workflowModules ?? []),
104
110
  ],
105
- workflowModules: [...existingWorkflowModules, workflowInterceptorsPath],
106
111
  },
107
112
  sinks: {
108
- ...existingSinks,
109
- ...braintrustSinks,
113
+ ...createBraintrustSinks(),
114
+ ...(options.sinks ?? {}),
110
115
  },
111
116
  };
112
117
  }
@@ -1,2 +0,0 @@
1
- # Braintrust API key for tracing
2
- BRAINTRUST_API_KEY=your-api-key-here
@@ -1,4 +0,0 @@
1
- server: temporal server start-dev
2
- worker1: pnpm run worker
3
- worker2: pnpm run worker
4
- worker3: pnpm run worker
@@ -1,183 +0,0 @@
1
- # Temporal + Braintrust Tracing Example
2
-
3
- This example demonstrates how to integrate Braintrust tracing with Temporal workflows and activities.
4
-
5
- ## Prerequisites
6
-
7
- - [mise](https://mise.jdx.dev/) (recommended) - automatically installs all dependencies
8
- - OR manually install:
9
- - Node.js 20+
10
- - `pnpm`
11
- - Temporal CLI (`temporal`)
12
- - Optional: [`overmind`](https://github.com/DarthSim/overmind) (only if you want to use the included `Procfile`)
13
-
14
- ### Option 1: Using mise (recommended)
15
-
16
- [mise](https://mise.jdx.dev/) will automatically install and manage all required tools (Node.js, Temporal CLI, overmind, and dependencies):
17
-
18
- **Install mise:**
19
-
20
- ```bash
21
- # macOS/Linux
22
- curl https://mise.run | sh
23
-
24
- # Or using Homebrew
25
- brew install mise
26
- ```
27
-
28
- **Setup and run:**
29
-
30
- ```bash
31
- # Copy and configure environment
32
- cp .env.example .env
33
- # Edit .env with your BRAINTRUST_API_KEY
34
-
35
- # mise will automatically install tools and dependencies
36
- mise run server # Start temporal server and workers
37
-
38
- # In another terminal:
39
- mise run workflow # Run the workflow client
40
- ```
41
-
42
- **Available mise tasks:**
43
-
44
- ```bash
45
- mise run install # Install dependencies
46
- mise run server # Run temporal server and workers
47
- mise run workflow # Run workflow client
48
- mise run stop # Stop temporal server and workers
49
- mise run kill # Force kill all processes
50
- ```
51
-
52
- ### Option 2: Manual installation
53
-
54
- #### Installing Temporal CLI
55
-
56
- The Temporal CLI is required to run the local Temporal server:
57
-
58
- **macOS:**
59
-
60
- ```bash
61
- brew install temporal
62
- ```
63
-
64
- **Linux:**
65
-
66
- ```bash
67
- # Using Homebrew
68
- brew install temporal
69
-
70
- # Or using curl
71
- curl -sSf https://temporal.download/cli.sh | sh
72
- ```
73
-
74
- **Windows:**
75
-
76
- ```powershell
77
- # Using Scoop
78
- scoop install temporal
79
-
80
- # Or download from releases
81
- # https://github.com/temporalio/cli/releases
82
- ```
83
-
84
- Verify the installation:
85
-
86
- ```bash
87
- temporal --version
88
- ```
89
-
90
- #### Installing overmind (optional)
91
-
92
- Overmind is a process manager that makes it easy to run multiple services together. If you want to use `overmind start` to run everything at once, install it for your platform:
93
-
94
- **macOS:**
95
-
96
- ```bash
97
- brew install overmind
98
- ```
99
-
100
- **Linux:**
101
-
102
- ```bash
103
- brew install overmind
104
-
105
- # Or download from releases
106
- # https://github.com/DarthSim/overmind/releases
107
- ```
108
-
109
- **Windows:**
110
- Overmind is not officially supported on Windows. Use the manual approach below instead.
111
-
112
- ## Setup
113
-
114
- ```bash
115
- # Copy and configure environment
116
- cp .env.example .env
117
- # Edit .env with your BRAINTRUST_API_KEY
118
-
119
- # Install dependencies
120
- pnpm install
121
- ```
122
-
123
- ## Running the Example
124
-
125
- ### Option 1: Using overmind
126
-
127
- Start the temporal server and workers together:
128
-
129
- ```bash
130
- overmind start
131
- ```
132
-
133
- Then in another terminal, run the workflow:
134
-
135
- ```bash
136
- pnpm run client
137
- ```
138
-
139
- ### Option 2: Manual
140
-
141
- 1. Start the Temporal server:
142
-
143
- ```bash
144
- temporal server start-dev
145
- ```
146
-
147
- 2. Start the worker:
148
-
149
- ```bash
150
- pnpm run worker
151
- ```
152
-
153
- 3. Run the client:
154
-
155
- ```bash
156
- pnpm run client
157
-
158
- # Or with a signal:
159
- pnpm run client -- --signal
160
- ```
161
-
162
- ## What Gets Traced
163
-
164
- - **Client span**: Wraps the workflow execution call
165
- - **Workflow span**: Created via sinks when the workflow starts
166
- - **Activity spans**: Created for each activity execution with parent linking
167
-
168
- The trace hierarchy looks like:
169
-
170
- ```
171
- Client span ("example.temporal.workflow")
172
- └── Workflow span ("temporal.workflow.simpleWorkflow")
173
- └── Activity span ("temporal.activity.addTen")
174
- └── Activity span ("temporal.activity.multiplyByTwo")
175
- └── Activity span ("temporal.activity.subtractFive")
176
- ```
177
-
178
- ## How It Works
179
-
180
- 1. **Client interceptor**: Captures the current Braintrust span context and adds it to workflow headers
181
- 2. **Workflow interceptor**: Extracts parent context from headers and creates a workflow span via sinks
182
- 3. **Sinks**: Allow the workflow isolate to call into Node.js to create spans (with `callDuringReplay: false`)
183
- 4. **Activity interceptor**: Creates spans for each activity, using the workflow span as parent
@@ -1,40 +0,0 @@
1
- # Mise will automatically read and use .tool-versions files as well as this file.
2
- [settings]
3
- experimental=true
4
-
5
- [env]
6
- # See env.example to configure API keys.
7
- _.file = ".env"
8
-
9
- [tools]
10
- node = "20"
11
- temporal = "latest"
12
- overmind = "latest"
13
-
14
- [hooks]
15
- postinstall = "mise run install"
16
-
17
- [tasks.install]
18
- description = "Install dependencies"
19
- run = "pnpm install --ignore-workspace"
20
-
21
- [tasks.server]
22
- description = "Run temporal server and workers"
23
- run = "overmind s"
24
-
25
- [tasks.workflow]
26
- description = "Run workflow client"
27
- run = "pnpm exec ts-node src/client.ts"
28
-
29
- [tasks.stop]
30
- description = "Stop temporal server and workers"
31
- run = "overmind quit || true"
32
-
33
- [tasks.kill]
34
- description = "Force kill temporal server and workers"
35
- run = """
36
- pkill -f 'examples/temporal.*ts-node' 2>/dev/null || true
37
- pkill -f 'overmind.*temporal' 2>/dev/null || true
38
- rm -f .overmind.sock
39
- echo 'Server killed'
40
- """
@@ -1,26 +0,0 @@
1
- {
2
- "name": "temporal-example",
3
- "version": "1.0.0",
4
- "private": true,
5
- "scripts": {
6
- "build": "tsc",
7
- "worker": "pnpm exec ts-node src/worker.ts",
8
- "client": "pnpm exec ts-node src/client.ts"
9
- },
10
- "dependencies": {
11
- "@braintrust/temporal": "^0.1.0",
12
- "@temporalio/activity": "^1.11.0",
13
- "@temporalio/client": "^1.11.0",
14
- "@temporalio/common": "^1.11.0",
15
- "@temporalio/worker": "^1.11.0",
16
- "@temporalio/workflow": "^1.11.0",
17
- "braintrust": "^2.0.0",
18
- "uuid": "^9.0.0"
19
- },
20
- "devDependencies": {
21
- "@types/node": "^20.0.0",
22
- "@types/uuid": "^9.0.0",
23
- "ts-node": "^10.9.0",
24
- "typescript": "^5.0.0"
25
- }
26
- }
@@ -1,42 +0,0 @@
1
- import * as braintrust from "braintrust";
2
-
3
- export interface TaskInput {
4
- value: number;
5
- }
6
-
7
- export async function addTen(input: TaskInput): Promise<number> {
8
- console.log(`Adding 10 to ${input.value}`);
9
-
10
- // Test child span within activity
11
- const result = await braintrust.traced(
12
- async (span) => {
13
- span.log({
14
- input: { value: input.value, operation: "add", operand: 10 },
15
- });
16
- await new Promise((resolve) => setTimeout(resolve, 500));
17
- const sum = input.value + 10;
18
- span.log({ output: sum });
19
- return sum;
20
- },
21
- { name: "compute.addition" },
22
- );
23
-
24
- console.log(`Result: ${input.value} + 10 = ${result}`);
25
- return result;
26
- }
27
-
28
- export async function multiplyByTwo(input: TaskInput): Promise<number> {
29
- console.log(`Multiplying ${input.value} by 2`);
30
- await new Promise((resolve) => setTimeout(resolve, 300));
31
- const result = input.value * 2;
32
- console.log(`Result: ${input.value} * 2 = ${result}`);
33
- return result;
34
- }
35
-
36
- export async function subtractFive(input: TaskInput): Promise<number> {
37
- console.log(`Subtracting 5 from ${input.value}`);
38
- await new Promise((resolve) => setTimeout(resolve, 300));
39
- const result = input.value - 5;
40
- console.log(`Result: ${input.value} - 5 = ${result}`);
41
- return result;
42
- }
@@ -1,53 +0,0 @@
1
- import { Client, Connection } from "@temporalio/client";
2
- import { v4 as uuid } from "uuid";
3
- import * as braintrust from "braintrust";
4
- import { BraintrustTemporalPlugin } from "@braintrust/temporal";
5
- import { simpleWorkflow } from "./workflows";
6
- import type { TaskInput } from "./activities";
7
-
8
- const TASK_QUEUE = "braintrust-example-task-queue";
9
-
10
- async function main() {
11
- braintrust.initLogger({ projectName: "temporal-example" });
12
-
13
- const connection = await Connection.connect({
14
- address: "localhost:7233",
15
- });
16
-
17
- const client = new Client({
18
- connection,
19
- namespace: "default",
20
- plugins: [new BraintrustTemporalPlugin()],
21
- });
22
-
23
- const inputData: TaskInput = { value: 5 };
24
- const workflowId = `simple-workflow-${uuid().slice(0, 8)}`;
25
-
26
- console.log(`Starting workflow with value: ${inputData.value}`);
27
- console.log(`Workflow ID: ${workflowId}`);
28
-
29
- // Wrap in a Braintrust span
30
- await braintrust.traced(
31
- async (span) => {
32
- const handle = await client.workflow.start(simpleWorkflow, {
33
- args: [inputData],
34
- taskQueue: TASK_QUEUE,
35
- workflowId,
36
- });
37
-
38
- const result = await handle.result();
39
- span.log({ output: result });
40
- console.log(`\nResult: ${result}`);
41
- console.log(`\nView trace: ${span.link()}`);
42
- return result;
43
- },
44
- { name: "temporal.client.simpleWorkflow" },
45
- );
46
-
47
- await braintrust.flush();
48
- }
49
-
50
- main().catch((err) => {
51
- console.error(err);
52
- process.exit(1);
53
- });
@@ -1,31 +0,0 @@
1
- import { Worker, NativeConnection } from "@temporalio/worker";
2
- import * as braintrust from "braintrust";
3
- import { BraintrustTemporalPlugin } from "@braintrust/temporal";
4
- import * as activities from "./activities";
5
-
6
- const TASK_QUEUE = "braintrust-example-task-queue";
7
-
8
- async function main() {
9
- braintrust.initLogger({ projectName: "temporal-example" });
10
-
11
- const connection = await NativeConnection.connect({
12
- address: "localhost:7233",
13
- });
14
-
15
- const worker = await Worker.create({
16
- connection,
17
- namespace: "default",
18
- taskQueue: TASK_QUEUE,
19
- workflowsPath: require.resolve("./workflows"),
20
- activities,
21
- plugins: [new BraintrustTemporalPlugin()],
22
- });
23
-
24
- console.log(`Worker started on task queue: ${TASK_QUEUE}`);
25
- await worker.run();
26
- }
27
-
28
- main().catch((err) => {
29
- console.error(err);
30
- process.exit(1);
31
- });
@@ -1,47 +0,0 @@
1
- import {
2
- proxyActivities,
3
- sleep,
4
- workflowInfo,
5
- defineSignal,
6
- setHandler,
7
- log,
8
- } from "@temporalio/workflow";
9
- import type * as activities from "./activities";
10
-
11
- const { addTen, multiplyByTwo, subtractFive } = proxyActivities<
12
- typeof activities
13
- >({
14
- startToCloseTimeout: "10 seconds",
15
- });
16
-
17
- export const addSignalValue = defineSignal<[number]>("addSignalValue");
18
-
19
- export interface TaskInput {
20
- value: number;
21
- }
22
-
23
- export async function simpleWorkflow(input: TaskInput): Promise<string> {
24
- log.info(`Starting workflow with value: ${input.value}`);
25
-
26
- let signalValue = 0;
27
- setHandler(addSignalValue, (value: number) => {
28
- log.info(`Received signal with value: ${value}`);
29
- signalValue += value;
30
- });
31
-
32
- // Step 1: Add 10
33
- const step1 = await addTen({ value: input.value });
34
- log.info(`After step 1: ${step1}`);
35
-
36
- // Step 2: Multiply by 2
37
- const step2 = await multiplyByTwo({ value: step1 });
38
- log.info(`After step 2: ${step2}`);
39
-
40
- // Step 3: Subtract 5
41
- const step3 = await subtractFive({ value: step2 });
42
- log.info(`After step 3: ${step3}`);
43
-
44
- const finalResult = `Complete: ${input.value} -> +10=${step1} -> *2=${step2} -> -5=${step3} + signal(${signalValue}) = ${step3 + signalValue}`;
45
- log.info(finalResult);
46
- return finalResult;
47
- }
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2021",
4
- "module": "commonjs",
5
- "lib": ["ES2021"],
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "outDir": "./dist",
11
- "rootDir": "./src",
12
- "declaration": true
13
- },
14
- "include": ["src/**/*"]
15
- }
@@ -1,243 +0,0 @@
1
- import { expect, test, describe } from "vitest";
2
- import {
3
- serializeHeaderValue,
4
- deserializeHeaderValue,
5
- BRAINTRUST_SPAN_HEADER,
6
- BRAINTRUST_WORKFLOW_SPAN_HEADER,
7
- BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
8
- } from "./utils";
9
- import { SpanComponentsV3, SpanObjectTypeV3 } from "braintrust/util";
10
- import {
11
- BraintrustTemporalPlugin,
12
- createBraintrustTemporalPlugin,
13
- } from "./plugin";
14
-
15
- describe("temporal header utilities", () => {
16
- test("serializeHeaderValue encodes string correctly", () => {
17
- const value = "test-span-id";
18
- const payload = serializeHeaderValue(value);
19
-
20
- expect(payload.metadata?.encoding).toBeDefined();
21
- expect(payload.data).toBeDefined();
22
- expect(new TextDecoder().decode(payload.metadata?.encoding)).toBe(
23
- "json/plain",
24
- );
25
- expect(new TextDecoder().decode(payload.data)).toBe('"test-span-id"');
26
- });
27
-
28
- test("deserializeHeaderValue decodes payload correctly", () => {
29
- const original = "test-value-123";
30
- const payload = serializeHeaderValue(original);
31
- const decoded = deserializeHeaderValue(payload);
32
-
33
- expect(decoded).toBe(original);
34
- });
35
-
36
- test("deserializeHeaderValue handles undefined payload", () => {
37
- expect(deserializeHeaderValue(undefined)).toBeUndefined();
38
- });
39
-
40
- test("deserializeHeaderValue handles payload without data", () => {
41
- expect(deserializeHeaderValue({ metadata: {} })).toBeUndefined();
42
- });
43
-
44
- test("deserializeHeaderValue handles invalid JSON", () => {
45
- const payload = {
46
- data: new TextEncoder().encode("not valid json"),
47
- };
48
- expect(deserializeHeaderValue(payload)).toBeUndefined();
49
- });
50
-
51
- test("round-trip serialization preserves complex strings", () => {
52
- const testCases = [
53
- "simple",
54
- "with spaces",
55
- "with/slashes",
56
- "unicode-日本語",
57
- "emoji-👋",
58
- "",
59
- ];
60
-
61
- for (const value of testCases) {
62
- const payload = serializeHeaderValue(value);
63
- const decoded = deserializeHeaderValue(payload);
64
- expect(decoded).toBe(value);
65
- }
66
- });
67
-
68
- test("header constants are defined", () => {
69
- expect(BRAINTRUST_SPAN_HEADER).toBe("_braintrust-span");
70
- expect(BRAINTRUST_WORKFLOW_SPAN_HEADER).toBe("_braintrust-workflow-span");
71
- expect(BRAINTRUST_WORKFLOW_SPAN_ID_HEADER).toBe(
72
- "_braintrust-workflow-span-id",
73
- );
74
- });
75
- });
76
-
77
- describe("SpanComponentsV3 cross-worker reconstruction", () => {
78
- test("can parse and reconstruct span components with new span_id", () => {
79
- const clientComponents = new SpanComponentsV3({
80
- object_type: SpanObjectTypeV3.PROJECT_LOGS,
81
- object_id: "project-123",
82
- row_id: "row-456",
83
- span_id: "client-span-id",
84
- root_span_id: "root-span-id",
85
- });
86
-
87
- const clientContext = clientComponents.toStr();
88
- const workflowSpanId = "workflow-span-id";
89
-
90
- const parsed = SpanComponentsV3.fromStr(clientContext);
91
- const data = parsed.data;
92
-
93
- expect(data.row_id).toBe("row-456");
94
- expect(data.root_span_id).toBe("root-span-id");
95
- expect(data.span_id).toBe("client-span-id");
96
-
97
- if (data.row_id && data.root_span_id) {
98
- const workflowComponents = new SpanComponentsV3({
99
- object_type: data.object_type,
100
- object_id: data.object_id,
101
- propagated_event: data.propagated_event,
102
- row_id: data.row_id,
103
- root_span_id: data.root_span_id,
104
- span_id: workflowSpanId,
105
- });
106
-
107
- const reconstructed = SpanComponentsV3.fromStr(
108
- workflowComponents.toStr(),
109
- );
110
- expect(reconstructed.data.span_id).toBe(workflowSpanId);
111
- expect(reconstructed.data.row_id).toBe("row-456");
112
- expect(reconstructed.data.root_span_id).toBe("root-span-id");
113
- expect(reconstructed.data.object_id).toBe("project-123");
114
- }
115
- });
116
-
117
- test("preserves object_type when reconstructing", () => {
118
- const objectTypes = [
119
- SpanObjectTypeV3.PROJECT_LOGS,
120
- SpanObjectTypeV3.EXPERIMENT,
121
- SpanObjectTypeV3.PLAYGROUND_LOGS,
122
- ];
123
-
124
- for (const objectType of objectTypes) {
125
- const original = new SpanComponentsV3({
126
- object_type: objectType,
127
- object_id: "test-id",
128
- row_id: "row-id",
129
- span_id: "original-span-id",
130
- root_span_id: "root-span-id",
131
- });
132
-
133
- const parsed = SpanComponentsV3.fromStr(original.toStr());
134
-
135
- const reconstructed = new SpanComponentsV3({
136
- object_type: parsed.data.object_type,
137
- object_id: parsed.data.object_id,
138
- propagated_event: parsed.data.propagated_event,
139
- row_id: parsed.data.row_id!,
140
- root_span_id: parsed.data.root_span_id!,
141
- span_id: "new-span-id",
142
- });
143
-
144
- expect(reconstructed.data.object_type).toBe(objectType);
145
- }
146
- });
147
-
148
- test("handles span components without row_id fields", () => {
149
- const componentsWithoutRowId = new SpanComponentsV3({
150
- object_type: SpanObjectTypeV3.PROJECT_LOGS,
151
- object_id: "test-id",
152
- });
153
-
154
- const parsed = SpanComponentsV3.fromStr(componentsWithoutRowId.toStr());
155
-
156
- expect(parsed.data.row_id).toBeUndefined();
157
- expect(parsed.data.span_id).toBeUndefined();
158
- expect(parsed.data.root_span_id).toBeUndefined();
159
- });
160
-
161
- test("preserves propagated_event when reconstructing", () => {
162
- const propagatedEvent = { key: "value", nested: { inner: 123 } };
163
-
164
- const original = new SpanComponentsV3({
165
- object_type: SpanObjectTypeV3.PROJECT_LOGS,
166
- object_id: "test-id",
167
- propagated_event: propagatedEvent,
168
- row_id: "row-id",
169
- span_id: "original-span-id",
170
- root_span_id: "root-span-id",
171
- });
172
-
173
- const parsed = SpanComponentsV3.fromStr(original.toStr());
174
-
175
- const reconstructed = new SpanComponentsV3({
176
- object_type: parsed.data.object_type,
177
- object_id: parsed.data.object_id,
178
- propagated_event: parsed.data.propagated_event,
179
- row_id: parsed.data.row_id!,
180
- root_span_id: parsed.data.root_span_id!,
181
- span_id: "new-span-id",
182
- });
183
-
184
- expect(reconstructed.data.propagated_event).toEqual(propagatedEvent);
185
- });
186
- });
187
-
188
- describe("BraintrustTemporalPlugin", () => {
189
- test("createBraintrustTemporalPlugin returns a plugin instance", () => {
190
- const plugin = createBraintrustTemporalPlugin();
191
- expect(plugin).toBeInstanceOf(BraintrustTemporalPlugin);
192
- expect(plugin.name).toBe("braintrust");
193
- });
194
-
195
- test("plugin has configureClient method", () => {
196
- const plugin = createBraintrustTemporalPlugin();
197
- expect(typeof plugin.configureClient).toBe("function");
198
- });
199
-
200
- test("plugin has configureWorker method", () => {
201
- const plugin = createBraintrustTemporalPlugin();
202
- expect(typeof plugin.configureWorker).toBe("function");
203
- });
204
-
205
- test("configureClient adds workflow interceptor", () => {
206
- const plugin = createBraintrustTemporalPlugin();
207
- const options = {};
208
- const configured = plugin.configureClient(options);
209
-
210
- expect(configured.interceptors).toBeDefined();
211
- expect(configured.interceptors?.workflow).toBeDefined();
212
- expect(Array.isArray(configured.interceptors?.workflow)).toBe(true);
213
- expect(configured.interceptors?.workflow?.length).toBe(1);
214
- });
215
-
216
- test("configureClient preserves existing interceptors", () => {
217
- const plugin = createBraintrustTemporalPlugin();
218
- const existingInterceptor = { start: async (i: unknown, n: unknown) => n };
219
- const options = {
220
- interceptors: {
221
- workflow: [existingInterceptor],
222
- },
223
- };
224
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
- const configured = plugin.configureClient(options as any);
226
-
227
- expect(configured.interceptors?.workflow?.length).toBe(2);
228
- });
229
-
230
- test("configureWorker adds activity interceptor and sinks", () => {
231
- const plugin = createBraintrustTemporalPlugin();
232
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
- const options = {} as any;
234
- const configured = plugin.configureWorker(options);
235
-
236
- expect(configured.interceptors).toBeDefined();
237
- expect(configured.interceptors?.activity).toBeDefined();
238
- expect(Array.isArray(configured.interceptors?.activity)).toBe(true);
239
- expect(configured.interceptors?.activity?.length).toBe(1);
240
- expect(configured.sinks).toBeDefined();
241
- expect(configured.sinks?.braintrust).toBeDefined();
242
- });
243
- });