@braintrust/temporal 0.1.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.mjs ADDED
@@ -0,0 +1,257 @@
1
+ import {
2
+ BRAINTRUST_SPAN_HEADER,
3
+ BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
4
+ __require,
5
+ deserializeHeaderValue
6
+ } from "./chunk-KT66NY2E.mjs";
7
+
8
+ // src/interceptors.ts
9
+ import { defaultPayloadConverter } from "@temporalio/common";
10
+ import * as braintrust2 from "braintrust";
11
+ import { SpanComponentsV3 } from "braintrust/util";
12
+
13
+ // src/sinks.ts
14
+ import * as braintrust from "braintrust";
15
+ var workflowSpans = /* @__PURE__ */ new Map();
16
+ var workflowSpanExports = /* @__PURE__ */ new Map();
17
+ function getWorkflowSpanExport(runId) {
18
+ return workflowSpanExports.get(runId);
19
+ }
20
+ function createBraintrustSinks() {
21
+ return {
22
+ braintrust: {
23
+ workflowStarted: {
24
+ fn: (info, parentContext, workflowSpanId) => {
25
+ const span = braintrust.startSpan({
26
+ name: `temporal.workflow.${info.workflowType}`,
27
+ spanAttributes: { type: "task" },
28
+ parent: parentContext,
29
+ spanId: workflowSpanId,
30
+ event: {
31
+ metadata: {
32
+ "temporal.workflow_type": info.workflowType,
33
+ "temporal.workflow_id": info.workflowId,
34
+ "temporal.run_id": info.runId
35
+ }
36
+ }
37
+ });
38
+ workflowSpans.set(info.runId, span);
39
+ workflowSpanExports.set(info.runId, span.export());
40
+ },
41
+ callDuringReplay: false
42
+ },
43
+ workflowCompleted: {
44
+ fn: (info, error) => {
45
+ const span = workflowSpans.get(info.runId);
46
+ if (span) {
47
+ if (error) {
48
+ span.log({ error });
49
+ }
50
+ span.end();
51
+ workflowSpans.delete(info.runId);
52
+ workflowSpanExports.delete(info.runId);
53
+ }
54
+ },
55
+ callDuringReplay: false
56
+ }
57
+ }
58
+ };
59
+ }
60
+
61
+ // src/interceptors.ts
62
+ function createBraintrustClientInterceptor() {
63
+ return {
64
+ async start(input, next) {
65
+ const span = braintrust2.currentSpan();
66
+ if (span) {
67
+ const exported = await span.export();
68
+ if (exported) {
69
+ const payload = defaultPayloadConverter.toPayload(exported);
70
+ if (payload) {
71
+ return next({
72
+ ...input,
73
+ headers: {
74
+ ...input.headers,
75
+ [BRAINTRUST_SPAN_HEADER]: payload
76
+ }
77
+ });
78
+ }
79
+ }
80
+ }
81
+ return next(input);
82
+ },
83
+ async signal(input, next) {
84
+ return next(input);
85
+ },
86
+ async signalWithStart(input, next) {
87
+ const span = braintrust2.currentSpan();
88
+ if (span) {
89
+ const exported = await span.export();
90
+ if (exported) {
91
+ const payload = defaultPayloadConverter.toPayload(exported);
92
+ if (payload) {
93
+ return next({
94
+ ...input,
95
+ headers: {
96
+ ...input.headers,
97
+ [BRAINTRUST_SPAN_HEADER]: payload
98
+ }
99
+ });
100
+ }
101
+ }
102
+ }
103
+ return next(input);
104
+ }
105
+ };
106
+ }
107
+ var BraintrustActivityInterceptor = class {
108
+ constructor(ctx) {
109
+ this.ctx = ctx;
110
+ }
111
+ async execute(input, next) {
112
+ const info = this.ctx.info;
113
+ const runId = info.workflowExecution.runId;
114
+ let parent;
115
+ const spanExportPromise = getWorkflowSpanExport(runId);
116
+ if (spanExportPromise) {
117
+ try {
118
+ parent = await spanExportPromise;
119
+ } catch {
120
+ }
121
+ }
122
+ if (!parent && input.headers) {
123
+ const workflowSpanId = deserializeHeaderValue(
124
+ input.headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER]
125
+ );
126
+ const clientContext = deserializeHeaderValue(
127
+ input.headers[BRAINTRUST_SPAN_HEADER]
128
+ );
129
+ if (workflowSpanId && clientContext) {
130
+ try {
131
+ const clientComponents = SpanComponentsV3.fromStr(clientContext);
132
+ const clientData = clientComponents.data;
133
+ const hasTracingContext = !!clientData.root_span_id;
134
+ const hasObjectMetadata = !!clientData.object_id || !!clientData.compute_object_metadata_args;
135
+ if (hasTracingContext && hasObjectMetadata) {
136
+ const workflowComponents = new SpanComponentsV3({
137
+ object_type: clientData.object_type,
138
+ object_id: clientData.object_id || void 0,
139
+ compute_object_metadata_args: clientData.object_id ? void 0 : clientData.compute_object_metadata_args || void 0,
140
+ propagated_event: clientData.propagated_event,
141
+ row_id: workflowSpanId,
142
+ // Use workflow's row_id, not client's
143
+ span_id: workflowSpanId,
144
+ // Use workflow's span_id, not client's
145
+ root_span_id: clientData.root_span_id
146
+ // Keep same trace
147
+ });
148
+ parent = workflowComponents.toStr();
149
+ } else {
150
+ parent = clientContext;
151
+ }
152
+ } catch {
153
+ parent = clientContext;
154
+ }
155
+ } else if (clientContext) {
156
+ parent = clientContext;
157
+ }
158
+ }
159
+ const span = braintrust2.startSpan({
160
+ name: `temporal.activity.${info.activityType}`,
161
+ spanAttributes: { type: "task" },
162
+ parent,
163
+ event: {
164
+ metadata: {
165
+ "temporal.activity_type": info.activityType,
166
+ "temporal.activity_id": info.activityId,
167
+ "temporal.workflow_id": info.workflowExecution.workflowId,
168
+ "temporal.workflow_run_id": runId
169
+ }
170
+ }
171
+ });
172
+ try {
173
+ const result = await braintrust2.withCurrent(span, () => next(input));
174
+ span.log({ output: result });
175
+ span.end();
176
+ return result;
177
+ } catch (e) {
178
+ span.log({ error: String(e) });
179
+ span.end();
180
+ throw e;
181
+ }
182
+ }
183
+ };
184
+ function createBraintrustActivityInterceptor(ctx) {
185
+ return {
186
+ inbound: new BraintrustActivityInterceptor(ctx)
187
+ };
188
+ }
189
+
190
+ // src/plugin.ts
191
+ var BraintrustTemporalPlugin = class {
192
+ get name() {
193
+ return "braintrust";
194
+ }
195
+ /**
196
+ * Configure the Temporal Client with Braintrust interceptors.
197
+ * Adds the client interceptor for propagating span context to workflows.
198
+ */
199
+ configureClient(options) {
200
+ const existing = options.interceptors?.workflow;
201
+ const braintrustInterceptor = createBraintrustClientInterceptor();
202
+ let workflow;
203
+ if (Array.isArray(existing)) {
204
+ workflow = [...existing, braintrustInterceptor];
205
+ } else if (existing) {
206
+ workflow = {
207
+ ...existing,
208
+ ...braintrustInterceptor
209
+ };
210
+ } else {
211
+ workflow = [braintrustInterceptor];
212
+ }
213
+ return {
214
+ ...options,
215
+ interceptors: {
216
+ ...options.interceptors,
217
+ workflow
218
+ }
219
+ };
220
+ }
221
+ /**
222
+ * 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.
225
+ */
226
+ 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
+ return {
235
+ ...options,
236
+ interceptors: {
237
+ ...options.interceptors,
238
+ activity: [
239
+ ...existingActivityInterceptors,
240
+ createBraintrustActivityInterceptor
241
+ ],
242
+ workflowModules: [...existingWorkflowModules, workflowInterceptorsPath]
243
+ },
244
+ sinks: {
245
+ ...existingSinks,
246
+ ...braintrustSinks
247
+ }
248
+ };
249
+ }
250
+ };
251
+ function createBraintrustTemporalPlugin() {
252
+ return new BraintrustTemporalPlugin();
253
+ }
254
+ export {
255
+ BraintrustTemporalPlugin,
256
+ createBraintrustTemporalPlugin
257
+ };
@@ -0,0 +1,13 @@
1
+ import { WorkflowInterceptorsFactory } from '@temporalio/workflow';
2
+
3
+ /**
4
+ * Workflow interceptors for Braintrust tracing.
5
+ *
6
+ * IMPORTANT: This module is loaded into the Temporal workflow isolate.
7
+ * It cannot import Node.js modules or access external state directly.
8
+ * Communication with the outside world is done via sinks.
9
+ */
10
+
11
+ declare const interceptors: WorkflowInterceptorsFactory;
12
+
13
+ export { interceptors };
@@ -0,0 +1,13 @@
1
+ import { WorkflowInterceptorsFactory } from '@temporalio/workflow';
2
+
3
+ /**
4
+ * Workflow interceptors for Braintrust tracing.
5
+ *
6
+ * IMPORTANT: This module is loaded into the Temporal workflow isolate.
7
+ * It cannot import Node.js modules or access external state directly.
8
+ * Communication with the outside world is done via sinks.
9
+ */
10
+
11
+ declare const interceptors: WorkflowInterceptorsFactory;
12
+
13
+ export { interceptors };
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/workflow-interceptors.ts
21
+ var workflow_interceptors_exports = {};
22
+ __export(workflow_interceptors_exports, {
23
+ interceptors: () => interceptors
24
+ });
25
+ module.exports = __toCommonJS(workflow_interceptors_exports);
26
+ var import_workflow = require("@temporalio/workflow");
27
+
28
+ // src/utils.ts
29
+ var BRAINTRUST_SPAN_HEADER = "_braintrust-span";
30
+ var BRAINTRUST_WORKFLOW_SPAN_HEADER = "_braintrust-workflow-span";
31
+ var BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = "_braintrust-workflow-span-id";
32
+ function serializeHeaderValue(value) {
33
+ return {
34
+ metadata: {
35
+ encoding: new TextEncoder().encode("json/plain")
36
+ },
37
+ data: new TextEncoder().encode(JSON.stringify(value))
38
+ };
39
+ }
40
+ function deserializeHeaderValue(payload) {
41
+ if (!payload?.data) {
42
+ return void 0;
43
+ }
44
+ try {
45
+ const decoded = new TextDecoder().decode(payload.data);
46
+ return JSON.parse(decoded);
47
+ } catch {
48
+ return void 0;
49
+ }
50
+ }
51
+
52
+ // src/workflow-interceptors.ts
53
+ var { braintrust } = (0, import_workflow.proxySinks)();
54
+ var BraintrustWorkflowInboundInterceptor = class {
55
+ constructor(state) {
56
+ this.state = state;
57
+ }
58
+ async execute(input, next) {
59
+ const parentContext = input.headers ? deserializeHeaderValue(input.headers[BRAINTRUST_SPAN_HEADER]) : void 0;
60
+ this.state.parentContext = parentContext;
61
+ this.state.spanId = (0, import_workflow.uuid4)();
62
+ braintrust.workflowStarted(parentContext, this.state.spanId);
63
+ try {
64
+ const result = await next(input);
65
+ braintrust.workflowCompleted();
66
+ return result;
67
+ } catch (e) {
68
+ braintrust.workflowCompleted(e instanceof Error ? e.message : String(e));
69
+ throw e;
70
+ }
71
+ }
72
+ };
73
+ var BraintrustWorkflowOutboundInterceptor = class {
74
+ constructor(state) {
75
+ this.state = state;
76
+ }
77
+ getHeaders() {
78
+ const info = (0, import_workflow.workflowInfo)();
79
+ const headers = {};
80
+ headers[BRAINTRUST_WORKFLOW_SPAN_HEADER] = serializeHeaderValue(info.runId);
81
+ if (this.state.spanId) {
82
+ headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER] = serializeHeaderValue(
83
+ this.state.spanId
84
+ );
85
+ }
86
+ if (this.state.parentContext) {
87
+ headers[BRAINTRUST_SPAN_HEADER] = serializeHeaderValue(
88
+ this.state.parentContext
89
+ );
90
+ }
91
+ return headers;
92
+ }
93
+ scheduleActivity(input, next) {
94
+ return next({
95
+ ...input,
96
+ headers: {
97
+ ...input.headers,
98
+ ...this.getHeaders()
99
+ }
100
+ });
101
+ }
102
+ scheduleLocalActivity(input, next) {
103
+ return next({
104
+ ...input,
105
+ headers: {
106
+ ...input.headers,
107
+ ...this.getHeaders()
108
+ }
109
+ });
110
+ }
111
+ startChildWorkflowExecution(input, next) {
112
+ return next({
113
+ ...input,
114
+ headers: {
115
+ ...input.headers,
116
+ ...this.getHeaders()
117
+ }
118
+ });
119
+ }
120
+ };
121
+ var interceptors = () => {
122
+ const state = {
123
+ parentContext: void 0,
124
+ spanId: void 0
125
+ };
126
+ return {
127
+ inbound: [new BraintrustWorkflowInboundInterceptor(state)],
128
+ outbound: [new BraintrustWorkflowOutboundInterceptor(state)]
129
+ };
130
+ };
131
+ // Annotate the CommonJS export names for ESM import in node:
132
+ 0 && (module.exports = {
133
+ interceptors
134
+ });
@@ -0,0 +1,95 @@
1
+ import {
2
+ BRAINTRUST_SPAN_HEADER,
3
+ BRAINTRUST_WORKFLOW_SPAN_HEADER,
4
+ BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
5
+ deserializeHeaderValue,
6
+ serializeHeaderValue
7
+ } from "./chunk-KT66NY2E.mjs";
8
+
9
+ // src/workflow-interceptors.ts
10
+ import {
11
+ proxySinks,
12
+ workflowInfo,
13
+ uuid4
14
+ } from "@temporalio/workflow";
15
+ var { braintrust } = proxySinks();
16
+ var BraintrustWorkflowInboundInterceptor = class {
17
+ constructor(state) {
18
+ this.state = state;
19
+ }
20
+ async execute(input, next) {
21
+ const parentContext = input.headers ? deserializeHeaderValue(input.headers[BRAINTRUST_SPAN_HEADER]) : void 0;
22
+ this.state.parentContext = parentContext;
23
+ this.state.spanId = uuid4();
24
+ braintrust.workflowStarted(parentContext, this.state.spanId);
25
+ try {
26
+ const result = await next(input);
27
+ braintrust.workflowCompleted();
28
+ return result;
29
+ } catch (e) {
30
+ braintrust.workflowCompleted(e instanceof Error ? e.message : String(e));
31
+ throw e;
32
+ }
33
+ }
34
+ };
35
+ var BraintrustWorkflowOutboundInterceptor = class {
36
+ constructor(state) {
37
+ this.state = state;
38
+ }
39
+ getHeaders() {
40
+ const info = workflowInfo();
41
+ const headers = {};
42
+ headers[BRAINTRUST_WORKFLOW_SPAN_HEADER] = serializeHeaderValue(info.runId);
43
+ if (this.state.spanId) {
44
+ headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER] = serializeHeaderValue(
45
+ this.state.spanId
46
+ );
47
+ }
48
+ if (this.state.parentContext) {
49
+ headers[BRAINTRUST_SPAN_HEADER] = serializeHeaderValue(
50
+ this.state.parentContext
51
+ );
52
+ }
53
+ return headers;
54
+ }
55
+ scheduleActivity(input, next) {
56
+ return next({
57
+ ...input,
58
+ headers: {
59
+ ...input.headers,
60
+ ...this.getHeaders()
61
+ }
62
+ });
63
+ }
64
+ scheduleLocalActivity(input, next) {
65
+ return next({
66
+ ...input,
67
+ headers: {
68
+ ...input.headers,
69
+ ...this.getHeaders()
70
+ }
71
+ });
72
+ }
73
+ startChildWorkflowExecution(input, next) {
74
+ return next({
75
+ ...input,
76
+ headers: {
77
+ ...input.headers,
78
+ ...this.getHeaders()
79
+ }
80
+ });
81
+ }
82
+ };
83
+ var interceptors = () => {
84
+ const state = {
85
+ parentContext: void 0,
86
+ spanId: void 0
87
+ };
88
+ return {
89
+ inbound: [new BraintrustWorkflowInboundInterceptor(state)],
90
+ outbound: [new BraintrustWorkflowOutboundInterceptor(state)]
91
+ };
92
+ };
93
+ export {
94
+ interceptors
95
+ };
@@ -0,0 +1,2 @@
1
+ # Braintrust API key for tracing
2
+ BRAINTRUST_API_KEY=your-api-key-here
@@ -0,0 +1,4 @@
1
+ server: temporal server start-dev
2
+ worker1: pnpm run worker
3
+ worker2: pnpm run worker
4
+ worker3: pnpm run worker
@@ -0,0 +1,183 @@
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